use super::context::Context;
use contract_bridge::auction::Call;
use contract_bridge::{Bid, Strain, Suit};
use std::cell::Cell;
const LENGTH_CAP: u8 = 13;
const POINTS_CAP: u8 = 37;
std::thread_local! {
static NT_INVITE_INFERENCE: Cell<bool> = const { Cell::new(true) };
}
pub fn set_nt_invite_inference(on: bool) {
NT_INVITE_INFERENCE.with(|cell| cell.set(on));
}
fn nt_invite_inference() -> bool {
NT_INVITE_INFERENCE.with(Cell::get)
}
std::thread_local! {
static RUBENS_TRANSFER_READING: Cell<bool> = const { Cell::new(true) };
}
pub fn set_rubens_transfer_reading(on: bool) {
RUBENS_TRANSFER_READING.with(|cell| cell.set(on));
}
fn rubens_transfer_reading() -> bool {
RUBENS_TRANSFER_READING.with(Cell::get)
}
std::thread_local! {
static ALERT_READING: Cell<bool> = const { Cell::new(true) };
}
pub fn set_alert_reading(on: bool) {
ALERT_READING.with(|cell| cell.set(on));
}
fn alert_reading() -> bool {
ALERT_READING.with(Cell::get)
}
std::thread_local! {
static RULE_ACCEPT: Cell<bool> = const { Cell::new(false) };
static FALLBACK_PROJECTION: Cell<bool> = const { Cell::new(true) };
}
pub fn set_fallback_projection(on: bool) {
FALLBACK_PROJECTION.with(|cell| cell.set(on));
}
#[must_use]
pub fn fallback_projection_enabled() -> bool {
FALLBACK_PROJECTION.with(Cell::get)
}
std::thread_local! {
static CONTROL_BID_READING: Cell<bool> = const { Cell::new(true) };
}
pub fn set_control_bid_reading(on: bool) {
CONTROL_BID_READING.with(|cell| cell.set(on));
}
#[must_use]
pub(super) fn control_bid_reading() -> bool {
CONTROL_BID_READING.with(Cell::get)
}
pub fn set_rule_accept(on: bool) {
RULE_ACCEPT.with(|cell| cell.set(on));
}
#[must_use]
pub fn rule_accept_enabled() -> bool {
RULE_ACCEPT.with(Cell::get)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Range {
pub min: u8,
pub max: u8,
}
impl Range {
pub const FULL_LENGTH: Self = Self {
min: 0,
max: LENGTH_CAP,
};
pub const FULL_POINTS: Self = Self {
min: 0,
max: POINTS_CAP,
};
#[must_use]
pub const fn new(min: u8, max: u8) -> Self {
Self { min, max }
}
#[must_use]
const fn at_least(min: u8, cap: u8) -> Self {
Self { min, max: cap }
}
#[must_use]
pub const fn contains(self, n: u8) -> bool {
self.min <= n && n <= self.max
}
#[must_use]
pub fn intersect(self, other: Self) -> Self {
let min = self.min.max(other.min);
let max = self.max.min(other.max);
if min <= max {
Self { min, max }
} else {
Self {
min: self.min.min(other.min),
max: self.max.max(other.max),
}
}
}
#[must_use]
pub fn union(self, other: Self) -> Self {
Self {
min: self.min.min(other.min),
max: self.max.max(other.max),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Inference {
pub lengths: [Range; 4],
pub points: Range,
}
impl Inference {
#[must_use]
pub const fn unknown() -> Self {
Self {
lengths: [Range::FULL_LENGTH; 4],
points: Range::FULL_POINTS,
}
}
#[must_use]
pub const fn length(&self, suit: Suit) -> Range {
self.lengths[suit as usize]
}
fn narrow_length(&mut self, suit: Suit, range: Range) {
let slot = &mut self.lengths[suit as usize];
*slot = slot.intersect(range);
}
fn narrow_points(&mut self, range: Range) {
self.points = self.points.intersect(range);
}
#[must_use]
pub fn intersect(&self, other: &Self) -> Self {
let mut out = *self;
for suit in Suit::ASC {
out.narrow_length(suit, other.length(suit));
}
out.narrow_points(other.points);
out
}
#[must_use]
pub fn union(&self, other: &Self) -> Self {
let mut out = *self;
for suit in Suit::ASC {
out.lengths[suit as usize] = out.length(suit).union(other.length(suit));
}
out.points = out.points.union(other.points);
out
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum Relative {
Me = 0,
Lho = 1,
Partner = 2,
Rho = 3,
}
pub(crate) const fn relative_of(len: usize, index: usize) -> Relative {
match (len - index) % 4 {
0 => Relative::Me,
1 => Relative::Rho,
2 => Relative::Partner,
_ => Relative::Lho,
}
}
fn systems_on_overcall_strip(auction: &[Call]) -> Option<Vec<Call>> {
if !crate::bidding::american::nt_overcall_systems_on() {
return None;
}
let open = auction.iter().position(|&c| c != Call::Pass)?;
let Call::Bid(opening) = auction[open] else {
return None;
};
if opening.level.get() != 1 || !opening.strain.is_suit() {
return None;
}
if crate::bidding::american::nt_overcall_gladiator()
&& matches!(opening.strain, Strain::Hearts | Strain::Spades)
{
return None;
}
if auction.get(open + 1) != Some(&Call::Bid(Bid::new(1, Strain::Notrump))) {
return None;
}
let mut stripped = auction.to_vec();
stripped.remove(open);
Some(stripped)
}
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Inferences {
players: [Inference; 4],
#[cfg_attr(feature = "serde", serde(skip))]
control_bid: Option<(u8, Suit)>,
}
impl Inferences {
#[must_use]
pub const fn get(&self, who: Relative) -> &Inference {
&self.players[who as usize]
}
#[must_use]
pub const fn me(&self) -> &Inference {
self.get(Relative::Me)
}
#[must_use]
pub const fn partner(&self) -> &Inference {
self.get(Relative::Partner)
}
#[must_use]
pub const fn lho(&self) -> &Inference {
self.get(Relative::Lho)
}
#[must_use]
pub const fn rho(&self) -> &Inference {
self.get(Relative::Rho)
}
#[must_use]
pub fn narrowed_points(&self, who: Relative, points: Range) -> Self {
let mut copy = *self;
copy.players[who as usize].points = copy.players[who as usize].points.intersect(points);
copy
}
#[must_use]
pub fn read(context: &Context<'_>) -> Self {
if let Some(stripped) = systems_on_overcall_strip(context.auction()) {
return Self::read(&Context::new(context.vul(), &stripped));
}
let auction = context.auction();
let len = auction.len();
let mut players = [Inference::unknown(); 4];
let mut control_bid = None;
let Some(opening_index) = auction.iter().position(|&c| c != Call::Pass) else {
return Self {
players,
control_bid,
};
};
let Call::Bid(opening_bid) = auction[opening_index] else {
return Self {
players,
control_bid,
};
};
let opener_lane = opening_index % 4;
#[allow(clippy::cast_possible_truncation)]
let opener_seat = opening_index as u8 + 1;
let opening_artificial =
opening_bid.strain == Strain::Notrump || opening_bid == Bid::new(2, Strain::Clubs);
let defending_parity = (opener_lane + 1) % 2;
let read_nt_invite = nt_invite_inference();
let stayman = opening_bid == Bid::new(1, Strain::Notrump)
&& auction.get(opening_index + 2) == Some(&Call::Bid(Bid::new(2, Strain::Clubs)));
let mut lane_suits = [0u8; 4];
let mut lane_bids = [0u8; 4];
let mut side_acted = [false; 2];
let mut highest: Option<Bid> = None;
let (rubens_suppress, rubens_cue, rubens_transfer) = rubens_reading(auction);
let (overlay, suppressed) = project_authored(context);
let landy_relay = landy_advance_suppress(auction);
let multi = multi_reading(auction);
let woolsey_x = woolsey_x_reading(auction);
let dont = dont_reading(auction);
let meckwell = meckwell_reading(auction);
let two_suiter = two_suiter_reading(auction);
let penalty_x = penalty_x_reading(auction);
let penalty_latch_doubles = penalty_latch_double_reading(auction);
let overcall_double = responder_overcall_double_reading(auction, len);
let gladiator = gladiator_reading(auction);
let mut suppressed_so_far = 0u64;
for (index, &call) in auction.iter().enumerate() {
let lane = index % 4;
let who = relative_of(len, index) as usize;
let is_opening_side = lane % 2 == opener_lane % 2;
let first_action_of_side = !side_acted[lane % 2];
match call {
Call::Pass | Call::Redouble => {}
Call::Double => {
if !is_opening_side
&& first_action_of_side
&& index != opening_index
&& opening_bid.strain.is_suit()
{
players[who].narrow_points(Range::at_least(11, POINTS_CAP));
}
side_acted[lane % 2] = true;
}
Call::Bid(bid) => {
if index == opening_index {
apply_opening(&mut players[who], bid, opener_seat);
} else if let Some(suit) = bid.strain.suit() {
let over_one_notrump = is_opening_side
&& opening_bid == Bid::new(1, Strain::Notrump)
&& bid.level.get() == 3;
let stayman_artificial = stayman
&& is_opening_side
&& lane != opener_lane
&& lane_bids[lane] >= 1
&& bid.level.get() == 3
&& matches!(bid.strain, Strain::Hearts | Strain::Spades)
&& lane_suits[(lane + 2) % 4] & (1u8 << suit as u8) == 0;
let nt_blanket = is_opening_side && opening_artificial && !over_one_notrump;
let chain = stayman_artificial
|| nt_structure_artificial(auction, index, opening_index)
|| rubens_suppress.contains(&Some(index))
|| (index < 64 && suppressed >> index & 1 != 0)
|| landy_relay == Some(index)
|| multi.is_some_and(|m| m.suppresses(index))
|| woolsey_x.is_some_and(|w| w.suppresses(index))
|| dont.is_some_and(|d| d.suppresses(index))
|| meckwell.is_some_and(|m| m.suppresses(index))
|| two_suiter.is_some_and(|t| t.suppresses(index))
|| gladiator.is_some_and(|g| g.suppresses(index));
let slam = if control_bid_reading()
&& index != opening_index
&& is_opening_side
&& !side_acted[defending_parity]
&& (4..=5).contains(&bid.level.get())
&& !chain
&& (!nt_blanket || context.prefixes().is_some())
{
classify_high_bid(
auction,
index,
bid,
len,
opening_index,
&players,
&overlay,
suppressed_so_far,
)
} else {
HighBid::Unclaimed
};
let suppress = match slam {
HighBid::ToPlay => true,
HighBid::Control { trump, shower } => {
let floor = if shower == who { 6 } else { 3 };
players[who]
.narrow_length(trump, Range::at_least(floor, LENGTH_CAP));
players[who].narrow_points(Range::at_least(13, POINTS_CAP));
#[allow(clippy::cast_possible_truncation)]
{
control_bid = Some((index as u8, trump));
}
true
}
HighBid::Unclaimed => nt_blanket || chain,
};
if suppress && index < 64 {
suppressed_so_far |= 1 << index;
}
let opener_ladder_rebid = crate::bidding::american::opener_extras_ladder()
&& !side_acted[defending_parity]
&& is_opening_side
&& lane == opener_lane
&& lane_bids[lane] == 1
&& opening_bid.level.get() == 1
&& matches!(opening_bid.strain, Strain::Clubs | Strain::Diamonds);
if !suppress {
let jump = bid
.level
.get()
.saturating_sub(cheapest_level(highest, bid.strain));
let mask = 1u8 << suit as u8;
let i_bid_it = lane_suits[lane] & mask != 0;
let partner_bid_it = lane_suits[(lane + 2) % 4] & mask != 0;
if i_bid_it {
players[who].narrow_length(suit, Range::at_least(6, LENGTH_CAP));
} else if partner_bid_it {
players[who].narrow_length(suit, Range::at_least(3, LENGTH_CAP));
} else if over_one_notrump {
players[who].narrow_length(suit, Range::at_least(5, LENGTH_CAP));
} else if !is_opening_side && first_action_of_side {
let min = if jump >= 1 { 6 } else { 5 };
players[who].narrow_length(suit, Range::at_least(min, LENGTH_CAP));
if jump == 0 {
players[who].narrow_points(Range::at_least(8, POINTS_CAP));
}
} else if jump >= 1 {
if jump == 1 {
let floor = if opener_ladder_rebid { 4 } else { 6 };
players[who]
.narrow_length(suit, Range::at_least(floor, LENGTH_CAP));
}
} else {
players[who].narrow_length(suit, Range::at_least(4, LENGTH_CAP));
apply_response_points(
&mut players[who],
bid,
opening_bid,
is_opening_side
&& lane == (opener_lane + 2) % 4
&& lane_bids[lane] == 0
&& !side_acted[defending_parity],
);
}
}
}
if index != opening_index && !side_acted[defending_parity] {
let responder_lane = (opener_lane + 2) % 4;
let opener_rebid =
is_opening_side && lane == opener_lane && lane_bids[lane] == 1;
let responder_first =
is_opening_side && lane == responder_lane && lane_bids[lane] == 0;
let opening_one_suit =
opening_bid.level.get() == 1 && opening_bid.strain.is_suit();
if read_nt_invite
&& bid.strain == Strain::Notrump
&& opening_bid == Bid::new(1, Strain::Notrump)
&& responder_first
{
match bid.level.get() {
2 => {
if crate::bidding::american::notrump_minors()
== crate::bidding::american::EUROPEAN
{
players[who].narrow_points(Range::new(8, 9));
} else {
players[who].narrow_length(
Suit::Diamonds,
Range::at_least(5, LENGTH_CAP),
);
}
}
3 => players[who].narrow_points(Range::at_least(9, POINTS_CAP)),
_ => {}
}
} else if bid.strain == Strain::Notrump && opening_one_suit {
if opener_rebid {
let nt_jump = bid
.level
.get()
.saturating_sub(cheapest_level(highest, Strain::Notrump));
if bid.level.get() == 1 {
players[who].narrow_points(Range::new(12, 16));
} else if bid.level.get() == 2 && nt_jump >= 1 {
players[who].narrow_points(Range::new(18, 21));
}
} else if responder_first && bid.level.get() == 1 {
players[who].narrow_points(Range::new(6, 12));
}
} else if let Some(suit) = bid.strain.suit() {
let partner_bid_it =
lane_suits[(lane + 2) % 4] & (1 << suit as u8) != 0;
if responder_first && partner_bid_it {
let jump = bid
.level
.get()
.saturating_sub(cheapest_level(highest, bid.strain));
match jump {
0 => players[who].narrow_points(Range::new(6, 10)),
1 => players[who].narrow_points(Range::new(10, 12)),
_ => {}
}
}
}
}
if crate::bidding::american::opener_extras_ladder()
&& !side_acted[defending_parity]
&& is_opening_side
&& lane == opener_lane
&& lane_bids[lane] == 1
&& opening_bid.level.get() == 1
&& matches!(opening_bid.strain, Strain::Clubs | Strain::Diamonds)
&& let (Some(bid_suit), Some(opened)) =
(bid.strain.suit(), opening_bid.strain.suit())
{
let jump = bid
.level
.get()
.saturating_sub(cheapest_level(highest, bid.strain));
let responder_bid_it =
lane_suits[(lane + 2) % 4] & (1 << bid_suit as u8) != 0;
if bid_suit == opened {
if jump >= 1 {
players[who].narrow_points(Range::at_least(16, POINTS_CAP));
}
} else if !responder_bid_it {
let reverse = jump == 0
&& bid.level.get() == 2
&& (bid.strain as u8) > (Strain::from(opened) as u8);
let jump_shift = jump == 1;
if reverse || jump_shift {
players[who].narrow_length(opened, Range::at_least(5, LENGTH_CAP));
let floor = if jump_shift { 18 } else { 17 };
players[who].narrow_points(Range::at_least(floor, POINTS_CAP));
}
}
}
if crate::bidding::american::opener_major_jump_rebid()
&& !side_acted[defending_parity]
&& is_opening_side
&& lane == opener_lane
&& lane_bids[lane] == 1
&& opening_bid.level.get() == 1
&& matches!(opening_bid.strain, Strain::Hearts | Strain::Spades)
&& bid.strain == opening_bid.strain
&& bid
.level
.get()
.saturating_sub(cheapest_level(highest, bid.strain))
>= 1
{
players[who].narrow_points(Range::at_least(16, POINTS_CAP));
}
if stayman && is_opening_side && !side_acted[defending_parity] {
let responder_lane = (opener_lane + 2) % 4;
if index == opening_index + 2 {
if !crate::bidding::american::garbage_stayman()
&& !crate::bidding::american::crawling_stayman()
{
players[who].narrow_points(Range::at_least(8, POINTS_CAP));
}
} else if index == opening_index + 4 && lane == opener_lane {
match bid.strain {
Strain::Hearts => players[who]
.narrow_length(Suit::Hearts, Range::at_least(4, LENGTH_CAP)),
Strain::Spades => {
players[who].narrow_length(
Suit::Spades,
Range::at_least(4, LENGTH_CAP),
);
players[who].narrow_length(Suit::Hearts, Range::new(0, 3));
}
Strain::Diamonds => {
players[who].narrow_length(Suit::Hearts, Range::new(0, 3));
players[who].narrow_length(Suit::Spades, Range::new(0, 3));
}
_ => {}
}
} else if index == opening_index + 6 && lane == responder_lane {
let raise_of_major = bid
.strain
.suit()
.is_some_and(|s| lane_suits[opener_lane] & (1u8 << s as u8) != 0);
match (bid.level.get(), bid.strain) {
(2, Strain::Notrump) => {
players[who].narrow_points(Range::new(8, 9));
}
(3, Strain::Notrump) => {
players[who].narrow_points(Range::at_least(9, POINTS_CAP));
}
(3, s) if s.is_suit() && raise_of_major => {
players[who].narrow_points(Range::new(8, 9));
}
_ => {}
}
}
}
if let Some(suit) = bid.strain.suit() {
lane_suits[lane] |= 1 << suit as u8;
}
lane_bids[lane] += 1;
side_acted[lane % 2] = true;
if highest.is_none_or(|h| outranks(bid, h)) {
highest = Some(bid);
}
}
}
}
if let Some((cue_index, overcall_suit)) = rubens_cue {
let who = relative_of(len, cue_index) as usize;
players[who].narrow_length(overcall_suit, Range::at_least(3, LENGTH_CAP));
players[who].narrow_points(Range::at_least(10, POINTS_CAP));
}
if let Some((transfer_index, suit, min_len)) = rubens_transfer {
let who = relative_of(len, transfer_index);
if matches!(who, Relative::Me | Relative::Partner) {
let who = who as usize;
players[who].narrow_length(suit, Range::at_least(min_len, LENGTH_CAP));
players[who].narrow_points(Range::at_least(10, POINTS_CAP));
}
}
for (seat, projected) in overlay.iter().enumerate() {
players[seat] = players[seat].intersect(projected);
}
if let Some(multi) = multi {
let who = relative_of(len, multi.overcall_index) as usize;
match multi.kind {
MultiKind::Major => {
players[who].narrow_length(Suit::Clubs, Range::new(0, 4));
players[who].narrow_length(Suit::Diamonds, Range::new(0, 4));
}
MultiKind::Muiderberg(major) => {
let other = if major == Suit::Hearts {
Suit::Spades
} else {
Suit::Hearts
};
players[who].narrow_length(major, Range::new(5, 5));
players[who].narrow_length(other, Range::new(0, 3));
}
}
let floor = crate::bidding::american::woolsey_points().0;
players[who].narrow_points(Range::at_least(floor, POINTS_CAP));
}
if let Some(woolsey_x) = woolsey_x {
let who = relative_of(len, woolsey_x.double_index) as usize;
let floor = crate::bidding::american::woolsey_double_floor();
players[who].narrow_points(Range::at_least(floor, POINTS_CAP));
}
if let Some(dont) = dont {
let who = relative_of(len, dont.overcall_index) as usize;
match dont.kind {
DontKind::OneSuiter => players[who].narrow_length(Suit::Spades, Range::new(0, 3)),
DontKind::ClubsMajor => {
players[who].narrow_length(Suit::Clubs, Range::at_least(4, LENGTH_CAP));
}
DontKind::DiamondsMajor => {
players[who].narrow_length(Suit::Diamonds, Range::at_least(4, LENGTH_CAP));
}
DontKind::BothMajors => {
players[who].narrow_length(Suit::Hearts, Range::at_least(4, LENGTH_CAP));
players[who].narrow_length(Suit::Spades, Range::at_least(4, LENGTH_CAP));
}
}
players[who].narrow_points(Range::at_least(dont.floor, POINTS_CAP));
}
if let Some(meckwell) = meckwell {
let who = relative_of(len, meckwell.overcall_index) as usize;
match meckwell.kind {
MeckwellKind::TwoWayDouble => {}
MeckwellKind::ClubsMajor => {
players[who].narrow_length(Suit::Clubs, Range::at_least(4, LENGTH_CAP));
}
MeckwellKind::DiamondsMajor => {
players[who].narrow_length(Suit::Diamonds, Range::at_least(4, LENGTH_CAP));
}
}
players[who].narrow_points(Range::at_least(meckwell.floor, POINTS_CAP));
}
if let Some(two_suiter) = two_suiter {
let who = relative_of(len, two_suiter.index) as usize;
match two_suiter.michaels_om {
Some(om) => players[who].narrow_length(om, Range::at_least(5, LENGTH_CAP)),
None => {
players[who].narrow_length(Suit::Clubs, Range::at_least(5, LENGTH_CAP));
players[who].narrow_length(Suit::Diamonds, Range::at_least(5, LENGTH_CAP));
}
}
}
if let Some(gladiator) = gladiator {
let who = relative_of(len, gladiator.index);
if matches!(who, Relative::Me | Relative::Partner) {
let who = who as usize;
match gladiator.advance {
GladiatorAdvance::Relay => {
players[who].narrow_points(Range::new(0, 9));
}
GladiatorAdvance::Cue { o } => {
players[who].narrow_length(o, Range::at_least(4, LENGTH_CAP));
players[who].narrow_points(Range::at_least(8, POINTS_CAP));
}
GladiatorAdvance::Splinter { o, m } => {
players[who].narrow_length(o, Range::at_least(4, LENGTH_CAP));
players[who].narrow_length(m, Range::new(0, 1));
players[who].narrow_points(Range::at_least(10, POINTS_CAP));
}
GladiatorAdvance::BothMinors => {
players[who].narrow_length(Suit::Clubs, Range::at_least(5, LENGTH_CAP));
players[who].narrow_length(Suit::Diamonds, Range::at_least(5, LENGTH_CAP));
players[who].narrow_points(Range::at_least(10, POINTS_CAP));
}
GladiatorAdvance::Minor { o, minor } => {
players[who].narrow_length(o, Range::at_least(5, LENGTH_CAP));
players[who].narrow_length(minor, Range::at_least(5, LENGTH_CAP));
players[who].narrow_points(Range::at_least(10, POINTS_CAP));
}
GladiatorAdvance::ClubTransfer => {
players[who].narrow_length(Suit::Clubs, Range::at_least(6, LENGTH_CAP));
players[who].narrow_points(Range::new(0, 7));
}
}
}
}
if let Some(double_index) = penalty_x {
let who = relative_of(len, double_index) as usize;
let floor = crate::bidding::american::natural_double_floor();
players[who].narrow_points(Range::at_least(floor, POINTS_CAP));
}
for (double_index, suit) in penalty_latch_doubles {
let who = relative_of(len, double_index) as usize;
players[who].narrow_length(suit, Range::at_least(4, LENGTH_CAP));
}
if let Some(double_index) = overcall_double {
let who = relative_of(len, double_index) as usize;
players[who].narrow_points(Range::at_least(8, POINTS_CAP));
}
Self {
players,
control_bid,
}
}
#[must_use]
pub(super) fn control_bid(&self) -> Option<(u8, Suit)> {
self.control_bid
}
}
#[cfg(test)]
#[must_use]
pub(crate) fn authored_reading(context: &Context<'_>) -> Inferences {
Inferences {
players: project_authored(context).0,
control_bid: None,
}
}
enum HighBid {
ToPlay,
Control { trump: Suit, shower: usize },
Unclaimed,
}
#[allow(clippy::too_many_arguments)]
fn classify_high_bid(
auction: &[Call],
index: usize,
bid: Bid,
len: usize,
opening_index: usize,
players: &[Inference; 4],
overlay: &[Inference; 4],
suppressed_so_far: u64,
) -> HighBid {
let Some(suit) = bid.strain.suit() else {
return HighBid::Unclaimed;
};
let who = relative_of(len, index) as usize;
let partner = (who + 2) % 4;
let shown =
|seat: usize, s: Suit| players[seat].length(s).min >= 4 || overlay[seat].length(s).min >= 4;
if shown(who, suit) || shown(partner, suit) {
return HighBid::Unclaimed;
}
if !Suit::ASC.into_iter().any(|s| s != suit && shown(who, s)) {
return HighBid::ToPlay;
}
let mut first_shown: Option<(Suit, u8)> = None;
let mut highest_before: Option<Bid> = None;
for (j, &call) in auction.iter().enumerate().take(index).skip(opening_index) {
let Call::Bid(prior) = call else {
continue;
};
if j % 4 == index % 4 {
let shown_suit = if j < 64 && suppressed_so_far >> j & 1 != 0 {
Suit::ASC
.into_iter()
.filter(|&s| overlay[who].length(s).min >= 4)
.max_by_key(|&s| (overlay[who].length(s).min, s as u8))
} else {
prior.strain.suit()
};
if let Some(r) = shown_suit {
first_shown = Some((r, prior.level.get()));
break;
}
}
if highest_before.is_none_or(|h| outranks(prior, h)) {
highest_before = Some(prior);
}
}
let Some((r, r_level)) = first_shown else {
return HighBid::Unclaimed;
};
let response_to_partners_minor = r_level == 1
&& relative_of(len, opening_index) as usize == partner
&& matches!(auction[opening_index], Call::Bid(opening)
if opening.level.get() == 1
&& matches!(opening.strain.suit(), Some(Suit::Clubs | Suit::Diamonds)));
let discipline =
response_to_partners_minor && crate::bidding::american::longer_major_response();
let bypassed = match (discipline, r, suit) {
(true, Suit::Hearts, Suit::Spades) => true,
(true, Suit::Spades, Suit::Hearts) => false,
_ => {
(suit as u8) < (r as u8)
&& highest_before.is_none_or(|h| outranks(Bid::new(r_level, bid.strain), h))
}
};
if !bypassed {
return HighBid::ToPlay;
}
for j in (opening_index..index).rev() {
if j % 2 != index % 2 {
continue; }
let Call::Bid(prior) = auction[j] else {
continue;
};
let seat = relative_of(len, j) as usize;
if j < 64 && suppressed_so_far >> j & 1 != 0 {
let candidate = Suit::ASC
.into_iter()
.filter(|&s| {
s != suit
&& (overlay[seat].length(s).min >= 4 || players[seat].length(s).min >= 4)
})
.max_by_key(|&s| {
(
overlay[seat].length(s).min.max(players[seat].length(s).min),
s as u8,
)
});
if let Some(trump) = candidate {
return HighBid::Control {
trump,
shower: seat,
};
}
} else if let Some(s) = prior.strain.suit()
&& s != suit
{
return HighBid::Control {
trump: s,
shower: seat,
};
}
}
HighBid::Unclaimed
}
fn project_authored(context: &Context<'_>) -> ([Inference; 4], u64) {
let auction = context.auction();
let len = auction.len();
let mut players = [Inference::unknown(); 4];
let mut suppressed = 0u64;
let Some(prefixes) = context.prefixes() else {
return (players, suppressed);
};
let mut project_call = |index: usize, classifier: &dyn super::trie::Classifier| {
let (Some(&made), Some(rules)) = (auction.get(index), classifier.as_rules()) else {
return;
};
let projection = rules
.rules()
.iter()
.filter(|rule| rule.call() == made)
.map(|rule| rule.project(context))
.reduce(|acc, p| acc.union(&p));
let alerted = alert_reading()
&& rules
.rules()
.iter()
.any(|rule| rule.call() == made && rule.alert().is_some());
if let Some(projection) = projection.filter(|_| alerted) {
let who = relative_of(len, index) as usize;
players[who] = players[who].intersect(&projection);
if index < 64 {
suppressed |= 1 << index;
}
}
};
if fallback_projection_enabled() {
let trie = prefixes.root();
for index in 0..len {
if let Some(classifier) = trie.authoring_classifier(context, &auction[..index]) {
project_call(index, classifier);
}
}
} else {
for (prefix, classifier) in prefixes.clone() {
project_call(prefix.len(), classifier);
}
}
(players, suppressed)
}
#[cfg(test)]
fn artificial(projection: &Inference, made: Call) -> bool {
let Call::Bid(bid) = made else {
return false;
};
let named = bid.strain.suit();
Suit::ASC
.into_iter()
.any(|suit| Some(suit) != named && projection.length(suit).min >= 4)
}
fn nt_structure_artificial(auction: &[Call], index: usize, opening_index: usize) -> bool {
let resp_first = auction.get(opening_index + 2);
if crate::bidding::american::notrump_minors() == crate::bidding::american::EUROPEAN {
return matches!(
resp_first,
Some(&Call::Bid(b))
if b == Bid::new(2, Strain::Spades) || b == Bid::new(3, Strain::Clubs)
);
}
let entered = matches!(
resp_first,
Some(&Call::Bid(b))
if b == Bid::new(3, Strain::Clubs)
|| b == Bid::new(2, Strain::Notrump)
|| b == Bid::new(2, Strain::Spades)
);
if !entered {
return false;
}
let opener_puppet_major = index == opening_index + 4
&& resp_first == Some(&Call::Bid(Bid::new(3, Strain::Clubs)))
&& matches!(
auction.get(index),
Some(&Call::Bid(b))
if b.level.get() == 3 && matches!(b.strain, Strain::Hearts | Strain::Spades)
);
!opener_puppet_major
}
fn outranks(bid: Bid, highest: Bid) -> bool {
bid.level.get() > highest.level.get()
|| (bid.level.get() == highest.level.get() && bid.strain > highest.strain)
}
const fn cheapest_level(highest: Option<Bid>, strain: Strain) -> u8 {
match highest {
None => 1,
Some(h) if strain as u8 > h.strain as u8 => h.level.get(),
Some(h) => h.level.get() + 1,
}
}
#[allow(clippy::type_complexity)]
fn rubens_reading(
auction: &[Call],
) -> (
[Option<usize>; 2],
Option<(usize, Suit)>,
Option<(usize, Suit, u8)>,
) {
let none = ([None, None], None, None);
if !super::instinct::rubens_advances_enabled() {
return none;
}
let Some((x, y, overcall_index, level)) = super::instinct::overcall_shape(auction) else {
return none;
};
if auction.get(overcall_index + 1) != Some(&Call::Pass) {
return none;
}
let advance_index = overcall_index + 2;
let Some(&Call::Bid(advance)) = auction.get(advance_index) else {
return none;
};
if level == 2 {
return if advance == Bid::new(2, Strain::from(x)) {
([Some(advance_index), None], Some((advance_index, y)), None)
} else {
none
};
}
let Some(source) = advance.strain.suit() else {
return none;
};
if advance.level.get() != 2 || (source as u8) < (x as u8) || (source as u8) >= (y as u8) {
return none;
}
let target_suit = Suit::ASC[(source as u8 + 1) as usize];
let target = Strain::from(target_suit);
let completion = (matches!(
auction.get(advance_index + 1),
Some(Call::Pass | Call::Double)
) && auction.get(advance_index + 2) == Some(&Call::Bid(Bid::new(2, target))))
.then_some(advance_index + 2);
let transfer = rubens_transfer_reading().then_some(if target_suit == y {
(advance_index, y, 3)
} else {
(advance_index, target_suit, 5)
});
([Some(advance_index), completion], None, transfer)
}
fn landy_advance_suppress(auction: &[Call]) -> Option<usize> {
let on = crate::bidding::american::landy_range().is_some()
|| crate::bidding::american::woolsey_enabled();
if !on {
return None;
}
let opening_index = auction.iter().position(|&c| c != Call::Pass)?;
if auction[opening_index] != Call::Bid(Bid::new(1, Strain::Notrump)) {
return None;
}
let opener_parity = opening_index % 2;
let overcall_index = auction
.iter()
.enumerate()
.skip(opening_index + 1)
.find_map(|(index, &call)| match call {
Call::Pass => None,
Call::Bid(bid) if index % 2 != opener_parity => {
Some((bid == Bid::new(2, Strain::Clubs)).then_some(index))
}
_ => Some(None),
})
.flatten()?;
advancer_artificial(auction, overcall_index, opener_parity)
}
fn advancer_artificial(
auction: &[Call],
overcall_index: usize,
opener_parity: usize,
) -> Option<usize> {
auction
.iter()
.enumerate()
.skip(overcall_index + 1)
.find_map(|(index, &call)| match call {
Call::Bid(bid) if index % 2 != opener_parity => Some(
matches!(
bid,
b if b == Bid::new(2, Strain::Diamonds)
|| b == Bid::new(2, Strain::Hearts)
|| b == Bid::new(2, Strain::Spades)
)
.then_some(index),
),
_ => None,
})
.flatten()
}
#[derive(Clone, Copy)]
enum MultiKind {
Major,
Muiderberg(Suit),
}
#[derive(Clone, Copy)]
struct MultiReading {
overcall_index: usize,
kind: MultiKind,
advance_suppress: Option<usize>,
}
impl MultiReading {
fn suppresses(&self, index: usize) -> bool {
(matches!(self.kind, MultiKind::Major) && self.overcall_index == index)
|| self.advance_suppress == Some(index)
}
}
fn multi_reading(auction: &[Call]) -> Option<MultiReading> {
if !crate::bidding::american::woolsey_enabled() {
return None;
}
let opening_index = auction.iter().position(|&c| c != Call::Pass)?;
if auction[opening_index] != Call::Bid(Bid::new(1, Strain::Notrump)) {
return None;
}
let opener_parity = opening_index % 2;
let reading = auction
.iter()
.enumerate()
.skip(opening_index + 1)
.find_map(|(index, &call)| match call {
Call::Pass => None,
Call::Bid(bid) if index % 2 != opener_parity => {
let kind = if bid == Bid::new(2, Strain::Diamonds) {
Some(MultiKind::Major)
} else if bid == Bid::new(2, Strain::Hearts) {
Some(MultiKind::Muiderberg(Suit::Hearts))
} else if bid == Bid::new(2, Strain::Spades) {
Some(MultiKind::Muiderberg(Suit::Spades))
} else {
None
};
Some(kind.map(|kind| MultiReading {
overcall_index: index,
kind,
advance_suppress: None,
}))
}
_ => Some(None),
})
.flatten()?;
let advance_suppress = matches!(reading.kind, MultiKind::Major)
.then(|| advancer_artificial(auction, reading.overcall_index, opener_parity))
.flatten();
Some(MultiReading {
advance_suppress,
..reading
})
}
#[derive(Clone, Copy)]
struct TwoSuiterReading {
index: usize,
michaels_om: Option<Suit>,
}
impl TwoSuiterReading {
const fn suppresses(self, index: usize) -> bool {
self.michaels_om.is_some() && index == self.index
}
}
fn two_suiter_reading(auction: &[Call]) -> Option<TwoSuiterReading> {
if !crate::bidding::american::uvu_over_majors() {
return None;
}
let opening_index = auction.iter().position(|&c| c != Call::Pass)?;
let Call::Bid(opening) = auction[opening_index] else {
return None;
};
if opening.level.get() != 1 || !matches!(opening.strain, Strain::Hearts | Strain::Spades) {
return None;
}
let Some(&Call::Bid(direct)) = auction.get(opening_index + 1) else {
return None;
};
let index = opening_index + 1;
if direct == Bid::new(2, opening.strain) {
let om = if opening.strain == Strain::Hearts {
Suit::Spades
} else {
Suit::Hearts
};
Some(TwoSuiterReading {
index,
michaels_om: Some(om),
})
} else if direct == Bid::new(2, Strain::Notrump) {
Some(TwoSuiterReading {
index,
michaels_om: None,
})
} else {
None
}
}
#[derive(Clone, Copy)]
enum GladiatorAdvance {
Relay,
Cue { o: Suit },
Splinter { o: Suit, m: Suit },
BothMinors,
Minor { o: Suit, minor: Suit },
ClubTransfer,
}
#[derive(Clone, Copy)]
struct GladiatorReading {
index: usize,
advance: GladiatorAdvance,
suppress: u64,
}
impl GladiatorReading {
const fn suppresses(self, index: usize) -> bool {
index < 64 && self.suppress >> index & 1 != 0
}
}
fn gladiator_reading(auction: &[Call]) -> Option<GladiatorReading> {
if !crate::bidding::american::nt_overcall_gladiator() {
return None;
}
let open = auction.iter().position(|&c| c != Call::Pass)?;
let Call::Bid(opening) = auction[open] else {
return None;
};
let m = opening.strain.suit()?;
if opening.level.get() != 1 || !matches!(m, Suit::Hearts | Suit::Spades) {
return None;
}
if auction.get(open + 1) != Some(&Call::Bid(Bid::new(1, Strain::Notrump)))
|| auction.get(open + 2) != Some(&Call::Pass)
{
return None;
}
let index = open + 3;
let Some(&Call::Bid(bid)) = auction.get(index) else {
return None;
};
let o = if m == Suit::Hearts {
Suit::Spades
} else {
Suit::Hearts
};
let mut suppress = 0u64;
let advance = if bid == Bid::new(2, Strain::Clubs) {
suppress |= 1 << index;
if auction.get(index + 2) == Some(&Call::Bid(Bid::new(2, Strain::Diamonds))) {
suppress |= 1 << (index + 2);
}
GladiatorAdvance::Relay
} else if bid == Bid::new(2, opening.strain) {
suppress |= 1 << index;
GladiatorAdvance::Cue { o }
} else if bid == Bid::new(3, opening.strain) {
suppress |= 1 << index;
GladiatorAdvance::Splinter { o, m }
} else if bid == Bid::new(4, opening.strain) {
suppress |= 1 << index;
GladiatorAdvance::BothMinors
} else if bid == Bid::new(2, Strain::Notrump) {
suppress |= 1 << index;
if auction.get(index + 2) == Some(&Call::Bid(Bid::new(3, Strain::Clubs))) {
suppress |= 1 << (index + 2);
}
GladiatorAdvance::ClubTransfer
} else if bid == Bid::new(4, Strain::Clubs) {
GladiatorAdvance::Minor {
o,
minor: Suit::Clubs,
}
} else if bid == Bid::new(4, Strain::Diamonds) {
GladiatorAdvance::Minor {
o,
minor: Suit::Diamonds,
}
} else {
return None;
};
Some(GladiatorReading {
index,
advance,
suppress,
})
}
#[derive(Clone, Copy)]
struct WoolseyXReading {
double_index: usize,
relay_suppress: Option<usize>,
}
impl WoolseyXReading {
fn suppresses(&self, index: usize) -> bool {
self.relay_suppress == Some(index)
}
}
fn woolsey_x_reading(auction: &[Call]) -> Option<WoolseyXReading> {
if !crate::bidding::american::woolsey_enabled() {
return None;
}
let opening_index = auction.iter().position(|&c| c != Call::Pass)?;
if auction[opening_index] != Call::Bid(Bid::new(1, Strain::Notrump)) {
return None;
}
let opener_parity = opening_index % 2;
let double_index = auction
.iter()
.enumerate()
.skip(opening_index + 1)
.find_map(|(index, &call)| match call {
Call::Pass => None,
Call::Double if index % 2 != opener_parity => Some(Some(index)),
_ => Some(None),
})
.flatten()?;
let relay_suppress = auction
.iter()
.enumerate()
.skip(double_index + 1)
.find_map(|(index, &call)| match call {
Call::Bid(bid) if index % 2 != opener_parity => {
Some((bid == Bid::new(2, Strain::Clubs)).then_some(index))
}
_ => None,
})
.flatten();
Some(WoolseyXReading {
double_index,
relay_suppress,
})
}
pub(super) fn penalty_x_reading(auction: &[Call]) -> Option<usize> {
use crate::bidding::american as a;
if !a::natural_defense_enabled()
|| a::direct_dont_enabled()
|| a::meckwell_enabled()
|| a::direct_landy_double().is_some()
|| a::woolsey_enabled()
{
return None;
}
let opening_index = auction.iter().position(|&c| c != Call::Pass)?;
if auction[opening_index] != Call::Bid(Bid::new(1, Strain::Notrump)) {
return None;
}
let opener_parity = opening_index % 2;
let double_index = auction
.iter()
.enumerate()
.skip(opening_index + 1)
.find_map(|(index, &call)| match call {
Call::Pass => None,
Call::Double if index % 2 != opener_parity => Some(Some(index)),
_ => Some(None),
})
.flatten()?;
(double_index % 4 >= opening_index).then_some(double_index)
}
fn responder_overcall_double_reading(auction: &[Call], len: usize) -> Option<usize> {
let opening_index = auction.iter().position(|&c| c != Call::Pass)?;
if auction[opening_index] != Call::Bid(Bid::new(1, Strain::Notrump))
|| opening_index % 2 != len % 2
{
return None;
}
match auction.get(opening_index + 1) {
Some(Call::Bid(bid)) if bid.strain.is_suit() => {}
_ => return None,
}
(auction.get(opening_index + 2) == Some(&Call::Double)).then_some(opening_index + 2)
}
fn penalty_latch_double_reading(auction: &[Call]) -> Vec<(usize, Suit)> {
if !crate::bidding::instinct::penalty_latch_enabled() {
return Vec::new();
}
let Some(x_index) = penalty_x_reading(auction) else {
return Vec::new();
};
let our_parity = x_index % 2;
let mut out = Vec::new();
let mut last_suit_bid: Option<(Suit, usize)> = None; for (index, &call) in auction.iter().enumerate().skip(x_index + 1) {
match call {
Call::Bid(bid) => {
last_suit_bid = bid.strain.suit().map(|suit| (suit, index % 2));
}
Call::Double if index % 2 == our_parity => {
if let Some((suit, bidder_parity)) = last_suit_bid
&& bidder_parity != our_parity
{
out.push((index, suit));
}
}
_ => {}
}
}
out
}
#[derive(Clone, Copy)]
enum DontKind {
OneSuiter,
ClubsMajor,
DiamondsMajor,
BothMajors,
}
#[derive(Clone, Copy)]
struct DontReading {
overcall_index: usize,
kind: DontKind,
floor: u8,
advance_suppress: Option<usize>,
}
impl DontReading {
fn suppresses(&self, index: usize) -> bool {
(!matches!(self.kind, DontKind::OneSuiter) && self.overcall_index == index)
|| self.advance_suppress == Some(index)
}
}
fn dont_reading(auction: &[Call]) -> Option<DontReading> {
if !crate::bidding::american::direct_dont_enabled() {
return None;
}
let opening_index = auction.iter().position(|&c| c != Call::Pass)?;
if auction[opening_index] != Call::Bid(Bid::new(1, Strain::Notrump)) {
return None;
}
let opener_parity = opening_index % 2;
let floor = crate::bidding::american::natural_overcall_points().0;
let (overcall_index, kind) = auction
.iter()
.enumerate()
.skip(opening_index + 1)
.find_map(|(index, &call)| match call {
Call::Pass => None,
Call::Double if index % 2 != opener_parity => Some(Some((index, DontKind::OneSuiter))),
Call::Bid(bid) if index % 2 != opener_parity => {
let kind = if bid == Bid::new(2, Strain::Clubs) {
Some(DontKind::ClubsMajor)
} else if bid == Bid::new(2, Strain::Diamonds) {
Some(DontKind::DiamondsMajor)
} else if bid == Bid::new(2, Strain::Hearts) {
Some(DontKind::BothMajors)
} else {
None
};
Some(kind.map(|kind| (index, kind)))
}
_ => Some(None),
})
.flatten()?;
let advance_suppress = match kind {
DontKind::OneSuiter => auction
.iter()
.enumerate()
.skip(overcall_index + 1)
.find_map(|(index, &call)| match call {
Call::Bid(bid) if index % 2 != opener_parity => {
Some((bid == Bid::new(2, Strain::Clubs)).then_some(index))
}
_ => None,
})
.flatten(),
_ => advancer_artificial(auction, overcall_index, opener_parity),
};
Some(DontReading {
overcall_index,
kind,
floor,
advance_suppress,
})
}
#[derive(Clone, Copy)]
enum MeckwellKind {
TwoWayDouble,
ClubsMajor,
DiamondsMajor,
}
#[derive(Clone, Copy)]
struct MeckwellReading {
overcall_index: usize,
kind: MeckwellKind,
floor: u8,
advance_suppress: Option<usize>,
}
impl MeckwellReading {
fn suppresses(&self, index: usize) -> bool {
(!matches!(self.kind, MeckwellKind::TwoWayDouble) && self.overcall_index == index)
|| self.advance_suppress == Some(index)
}
}
fn meckwell_reading(auction: &[Call]) -> Option<MeckwellReading> {
if !crate::bidding::american::meckwell_enabled() {
return None;
}
let opening_index = auction.iter().position(|&c| c != Call::Pass)?;
if auction[opening_index] != Call::Bid(Bid::new(1, Strain::Notrump)) {
return None;
}
let opener_parity = opening_index % 2;
let floor = crate::bidding::american::natural_overcall_points().0;
let (overcall_index, kind) = auction
.iter()
.enumerate()
.skip(opening_index + 1)
.find_map(|(index, &call)| match call {
Call::Pass => None,
Call::Double if index % 2 != opener_parity => {
Some(Some((index, MeckwellKind::TwoWayDouble)))
}
Call::Bid(bid) if index % 2 != opener_parity => {
let kind = if bid == Bid::new(2, Strain::Clubs) {
Some(MeckwellKind::ClubsMajor)
} else if bid == Bid::new(2, Strain::Diamonds) {
Some(MeckwellKind::DiamondsMajor)
} else {
None
};
Some(kind.map(|kind| (index, kind)))
}
_ => Some(None),
})
.flatten()?;
let advance_suppress = match kind {
MeckwellKind::TwoWayDouble => auction
.iter()
.enumerate()
.skip(overcall_index + 1)
.find_map(|(index, &call)| match call {
Call::Bid(bid) if index % 2 != opener_parity => {
Some((bid == Bid::new(2, Strain::Clubs)).then_some(index))
}
_ => None,
})
.flatten(),
_ => advancer_artificial(auction, overcall_index, opener_parity),
};
Some(MeckwellReading {
overcall_index,
kind,
floor,
advance_suppress,
})
}
fn apply_opening(inf: &mut Inference, bid: Bid, seat: u8) {
let light = crate::bidding::american::rule_of_20_enabled();
let major_floor = if seat >= 3 {
9
} else if light {
10
} else {
12
};
let minor_floor = if light { 10 } else { 12 };
let majors_light = Range::new(major_floor, 21);
match (bid.level.get(), bid.strain) {
(1, Strain::Hearts) => {
inf.narrow_length(Suit::Hearts, Range::at_least(5, LENGTH_CAP));
inf.narrow_points(majors_light);
}
(1, Strain::Spades) => {
inf.narrow_length(Suit::Spades, Range::at_least(5, LENGTH_CAP));
inf.narrow_points(majors_light);
}
(1, Strain::Diamonds) => {
inf.narrow_length(Suit::Diamonds, Range::at_least(3, LENGTH_CAP));
inf.narrow_length(Suit::Hearts, Range::new(0, 4));
inf.narrow_length(Suit::Spades, Range::new(0, 4));
inf.narrow_points(Range::new(minor_floor, 21));
}
(1, Strain::Clubs) => {
inf.narrow_length(Suit::Clubs, Range::at_least(3, LENGTH_CAP));
inf.narrow_length(Suit::Hearts, Range::new(0, 4));
inf.narrow_length(Suit::Spades, Range::new(0, 4));
inf.narrow_points(Range::new(minor_floor, 21));
}
(1, Strain::Notrump) => {
balanced(inf);
inf.narrow_points(Range::new(14, 19));
}
(2, Strain::Clubs) => {
inf.narrow_points(Range::at_least(20, POINTS_CAP));
}
(2, Strain::Notrump) => {
balanced(inf);
inf.narrow_points(Range::new(19, 23));
}
(2, strain) if strain.is_suit() => {
inf.narrow_length(strain.suit().unwrap(), Range::new(6, 6));
inf.narrow_points(Range::new(5, 10));
}
(3, strain) if strain.is_suit() => {
inf.narrow_length(strain.suit().unwrap(), Range::at_least(7, LENGTH_CAP));
inf.narrow_points(Range::new(0, 11));
}
_ => {}
}
}
fn balanced(inf: &mut Inference) {
for suit in Suit::ASC {
inf.narrow_length(suit, Range::new(2, 5));
}
}
fn apply_response_points(inf: &mut Inference, response: Bid, opening: Bid, eligible: bool) {
if !eligible {
return;
}
match response.level.get() {
1 => inf.narrow_points(Range::at_least(6, POINTS_CAP)),
2 if is_american(opening, response) => {
inf.narrow_points(Range::at_least(13, POINTS_CAP));
}
_ => {}
}
}
fn is_american(opening: Bid, response: Bid) -> bool {
response.level.get() == 2
&& match opening.strain {
Strain::Hearts | Strain::Spades => true,
Strain::Diamonds => response.strain == Strain::Clubs,
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bidding::constraint::point_count;
use contract_bridge::auction::RelativeVulnerability;
use contract_bridge::{Bid, Hand, Level};
use proptest::prelude::*;
const fn bid(level: u8, strain: Strain) -> Call {
Call::Bid(Bid {
level: Level::new(level),
strain,
})
}
fn read(auction: &[Call]) -> Inferences {
Inferences::read(&Context::new(RelativeVulnerability::NONE, auction))
}
fn read_booked(auction: &[Call]) -> Inferences {
let stance = crate::american().against(crate::bidding::Family::NATURAL);
Inferences::read(&stance.prefixed_context(RelativeVulnerability::NONE, auction))
}
#[test]
fn opening_shapes() {
let one_heart = read(&[bid(1, Strain::Hearts)]);
assert_eq!(one_heart.rho().length(Suit::Hearts), Range::new(5, 13));
assert_eq!(one_heart.rho().points, Range::new(10, 21));
let one_nt = read(&[bid(1, Strain::Notrump)]);
assert_eq!(one_nt.rho().length(Suit::Spades), Range::new(2, 5));
assert_eq!(one_nt.rho().points, Range::new(14, 19));
let two_clubs = read(&[bid(2, Strain::Clubs)]);
assert_eq!(two_clubs.rho().length(Suit::Spades), Range::FULL_LENGTH);
assert_eq!(two_clubs.rho().points, Range::new(20, 37));
let weak_two = read(&[bid(2, Strain::Spades)]);
assert_eq!(weak_two.rho().length(Suit::Spades), Range::new(6, 6));
assert_eq!(weak_two.rho().points, Range::new(5, 10));
let preempt = read(&[bid(3, Strain::Diamonds)]);
assert_eq!(preempt.rho().length(Suit::Diamonds), Range::new(7, 13));
let one_club = read(&[bid(1, Strain::Clubs)]);
assert_eq!(one_club.rho().length(Suit::Clubs), Range::new(3, 13));
assert_eq!(one_club.rho().length(Suit::Hearts), Range::new(0, 4));
}
#[test]
fn opener_extras_ladder_reads_extras() {
use crate::bidding::american::set_opener_extras_ladder;
let d = bid(1, Strain::Diamonds);
let s = bid(1, Strain::Spades);
let p = Call::Pass;
set_opener_extras_ladder(true);
let jr = read(&[d, p, s, p, bid(3, Strain::Diamonds), p]);
assert!(jr.partner().length(Suit::Diamonds).min >= 6);
assert!(jr.partner().points.min >= 16);
let rev = read(&[d, p, s, p, bid(2, Strain::Hearts), p]);
assert!(rev.partner().length(Suit::Diamonds).min >= 5);
assert!(rev.partner().length(Suit::Hearts).min >= 4);
assert!(rev.partner().points.min >= 17);
let js = read(&[d, p, s, p, bid(3, Strain::Clubs), p]);
assert!(js.partner().length(Suit::Diamonds).min >= 5);
assert!(js.partner().points.min >= 18);
assert_eq!(
js.partner().length(Suit::Clubs),
Range::at_least(4, LENGTH_CAP)
);
set_opener_extras_ladder(true);
}
#[test]
fn opener_major_jump_rebid_reads_extras() {
use crate::bidding::american::set_opener_major_jump_rebid;
let h = bid(1, Strain::Hearts);
let s = bid(1, Strain::Spades);
let p = Call::Pass;
set_opener_major_jump_rebid(true);
let jr = read(&[h, p, s, p, bid(3, Strain::Hearts), p]);
assert!(jr.partner().length(Suit::Hearts).min >= 6);
assert!(jr.partner().points.min >= 16);
set_opener_major_jump_rebid(true);
}
#[test]
fn high_bid_control_vs_natural() {
let control = read(&[
bid(1, Strain::Diamonds),
Call::Pass,
bid(1, Strain::Spades),
Call::Pass,
bid(2, Strain::Diamonds),
Call::Pass,
bid(4, Strain::Hearts),
Call::Pass,
]);
assert_eq!(control.partner().length(Suit::Hearts).min, 0);
assert!(control.partner().length(Suit::Diamonds).min >= 3);
assert!(control.partner().points.min >= 13);
let rebid = read(&[
bid(1, Strain::Diamonds),
Call::Pass,
bid(1, Strain::Spades),
Call::Pass,
bid(2, Strain::Diamonds),
Call::Pass,
bid(4, Strain::Spades),
Call::Pass,
]);
assert!(rebid.partner().length(Suit::Spades).min >= 6);
let preempt = read(&[
bid(1, Strain::Diamonds),
Call::Pass,
bid(4, Strain::Hearts),
Call::Pass,
]);
assert!(preempt.control_bid().is_none());
let above = read(&[
bid(1, Strain::Clubs),
Call::Pass,
bid(1, Strain::Hearts),
Call::Pass,
bid(2, Strain::Clubs),
Call::Pass,
bid(4, Strain::Spades),
Call::Pass,
]);
assert!(above.control_bid().is_none());
let post_transfer = read_booked(&[
bid(1, Strain::Notrump),
Call::Pass,
bid(2, Strain::Diamonds),
Call::Pass,
bid(2, Strain::Hearts),
Call::Pass,
bid(4, Strain::Spades),
Call::Pass,
]);
assert!(post_transfer.control_bid().is_none());
assert!(post_transfer.partner().length(Suit::Hearts).min >= 5);
let mirror = read_booked(&[
bid(1, Strain::Notrump),
Call::Pass,
bid(2, Strain::Hearts),
Call::Pass,
bid(2, Strain::Spades),
Call::Pass,
bid(4, Strain::Hearts),
Call::Pass,
]);
assert_eq!(mirror.partner().length(Suit::Hearts).min, 0);
assert!(mirror.partner().length(Suit::Spades).min >= 6);
}
#[test]
fn high_bid_under_longer_major_response() {
use crate::bidding::american::set_longer_major_response;
set_longer_major_response(true);
let control = read(&[
bid(1, Strain::Clubs),
Call::Pass,
bid(1, Strain::Hearts),
Call::Pass,
bid(2, Strain::Clubs),
Call::Pass,
bid(4, Strain::Spades),
Call::Pass,
]);
let to_play = read(&[
bid(1, Strain::Clubs),
Call::Pass,
bid(1, Strain::Spades),
Call::Pass,
bid(2, Strain::Clubs),
Call::Pass,
bid(4, Strain::Hearts),
Call::Pass,
]);
set_longer_major_response(false);
assert_eq!(control.partner().length(Suit::Spades).min, 0);
assert!(control.partner().length(Suit::Clubs).min >= 3);
assert!(control.partner().points.min >= 13);
assert!(to_play.control_bid().is_none());
let above = read(&[
bid(1, Strain::Clubs),
Call::Pass,
bid(1, Strain::Hearts),
Call::Pass,
bid(2, Strain::Clubs),
Call::Pass,
bid(4, Strain::Spades),
Call::Pass,
]);
assert!(above.control_bid().is_none());
}
#[test]
fn gambling_3nt_over_double_reads_unbalanced() {
use crate::bidding::instinct::set_gambling_3nt_over_double;
set_gambling_3nt_over_double(true);
let read = read_booked(&[
bid(1, Strain::Notrump),
Call::Double,
bid(3, Strain::Notrump),
Call::Pass,
]);
assert!(read.partner().length(Suit::Clubs).contains(6));
assert!(read.partner().length(Suit::Diamonds).contains(6));
set_gambling_3nt_over_double(false);
}
#[test]
fn leaping_michaels_conditions_partner() {
use crate::bidding::american::set_leaping_michaels;
set_leaping_michaels(true);
let advance = read_booked(&[bid(2, Strain::Hearts), bid(4, Strain::Clubs), Call::Pass]);
assert_eq!(advance.partner().length(Suit::Clubs), Range::new(5, 13));
assert_eq!(advance.partner().length(Suit::Spades), Range::new(5, 13));
assert_eq!(advance.partner().points, Range::new(14, 37));
let cue = read_booked(&[
bid(2, Strain::Diamonds),
bid(4, Strain::Diamonds),
Call::Pass,
]);
assert_eq!(cue.partner().length(Suit::Hearts), Range::new(5, 13));
assert_eq!(cue.partner().length(Suit::Spades), Range::new(5, 13));
set_leaping_michaels(false);
let off = read_booked(&[bid(2, Strain::Hearts), bid(4, Strain::Clubs), Call::Pass]);
assert_eq!(off.partner().length(Suit::Spades), Range::FULL_LENGTH);
}
#[test]
fn landy_conditions_partner() {
use crate::bidding::american::{set_landy, set_unusual_notrump_defense};
set_landy(Some((8, 15)));
set_unusual_notrump_defense(Some((8, 15)));
let advance = read_booked(&[bid(1, Strain::Notrump), bid(2, Strain::Clubs), Call::Pass]);
assert_eq!(advance.partner().length(Suit::Hearts), Range::new(4, 13));
assert_eq!(advance.partner().length(Suit::Spades), Range::new(4, 13));
assert_eq!(advance.partner().length(Suit::Clubs), Range::FULL_LENGTH);
assert_eq!(advance.partner().points, Range::new(8, 37));
let minors = read_booked(&[bid(1, Strain::Notrump), bid(2, Strain::Notrump), Call::Pass]);
assert_eq!(minors.partner().length(Suit::Clubs), Range::new(5, 13));
assert_eq!(minors.partner().length(Suit::Diamonds), Range::new(5, 13));
let relay = read_booked(&[
bid(1, Strain::Notrump),
bid(2, Strain::Clubs),
Call::Pass,
bid(2, Strain::Diamonds),
Call::Pass,
]);
assert_eq!(relay.partner().length(Suit::Diamonds), Range::FULL_LENGTH);
set_landy(None);
set_unusual_notrump_defense(None);
let off = read_booked(&[bid(1, Strain::Notrump), bid(2, Strain::Clubs), Call::Pass]);
assert_eq!(off.partner().length(Suit::Spades), Range::FULL_LENGTH);
set_unusual_notrump_defense(Some((8, 13)));
}
#[test]
fn woolsey_conditions_partner() {
use crate::bidding::american::{
set_landy, set_unusual_notrump_defense, set_woolsey, set_woolsey_points,
};
set_landy(None);
set_unusual_notrump_defense(None);
set_woolsey(true);
set_woolsey_points(10, 19);
let two_c = read_booked(&[bid(1, Strain::Notrump), bid(2, Strain::Clubs), Call::Pass]);
assert_eq!(two_c.partner().length(Suit::Hearts), Range::new(4, 5));
assert_eq!(two_c.partner().length(Suit::Spades), Range::new(4, 5));
assert_eq!(two_c.partner().length(Suit::Clubs), Range::FULL_LENGTH);
assert_eq!(two_c.partner().points, Range::new(10, 37));
let multi = read(&[
bid(1, Strain::Notrump),
bid(2, Strain::Diamonds),
Call::Pass,
]);
assert_eq!(multi.partner().length(Suit::Diamonds), Range::new(0, 4));
assert_eq!(multi.partner().length(Suit::Clubs), Range::new(0, 4));
let muiderberg = read(&[bid(1, Strain::Notrump), bid(2, Strain::Hearts), Call::Pass]);
assert_eq!(muiderberg.partner().length(Suit::Hearts), Range::new(5, 5));
assert_eq!(muiderberg.partner().length(Suit::Spades), Range::new(0, 3));
let pref_2c = read(&[
bid(1, Strain::Notrump),
bid(2, Strain::Clubs),
Call::Pass,
bid(2, Strain::Hearts),
Call::Pass,
]);
assert_eq!(pref_2c.partner().length(Suit::Hearts), Range::FULL_LENGTH);
let pref_2d = read(&[
bid(1, Strain::Notrump),
bid(2, Strain::Diamonds),
Call::Pass,
bid(2, Strain::Spades),
Call::Pass,
]);
assert_eq!(pref_2d.partner().length(Suit::Spades), Range::FULL_LENGTH);
set_woolsey(false);
let off = read(&[
bid(1, Strain::Notrump),
bid(2, Strain::Diamonds),
Call::Pass,
]);
assert_eq!(off.partner().length(Suit::Diamonds), Range::new(5, 13));
set_unusual_notrump_defense(Some((8, 13)));
}
#[test]
fn artificial_is_bid_only() {
let mut floors_hearts = Inference::unknown();
floors_hearts.narrow_length(Suit::Hearts, Range::at_least(5, LENGTH_CAP));
assert!(artificial(&floors_hearts, bid(2, Strain::Diamonds)));
assert!(!artificial(&floors_hearts, bid(1, Strain::Hearts)));
assert!(!artificial(&floors_hearts, Call::Pass));
assert!(!artificial(&floors_hearts, Call::Double));
}
#[test]
fn woolsey_double_and_advances_read() {
use crate::bidding::american::{
set_landy, set_unusual_notrump_defense, set_woolsey, set_woolsey_double_floor,
set_woolsey_points,
};
set_landy(None);
set_unusual_notrump_defense(None);
set_woolsey(true);
set_woolsey_points(10, 19);
set_woolsey_double_floor(12);
let x = read(&[bid(1, Strain::Notrump), Call::Double, Call::Pass]);
assert_eq!(x.partner().points, Range::new(12, 37));
let relay = read(&[
bid(1, Strain::Notrump),
Call::Double,
Call::Pass,
bid(2, Strain::Clubs),
Call::Pass,
]);
assert_eq!(relay.partner().length(Suit::Clubs), Range::FULL_LENGTH);
let ask = read(&[
bid(1, Strain::Notrump),
bid(2, Strain::Hearts),
Call::Pass,
bid(2, Strain::Notrump),
Call::Pass,
]);
assert_eq!(ask.partner().points, Range::new(0, 37));
set_woolsey(false);
let off = read(&[bid(1, Strain::Notrump), Call::Double, Call::Pass]);
assert_eq!(off.partner().points, Range::new(15, 37));
set_unusual_notrump_defense(Some((8, 13)));
}
#[test]
fn dont_overcalls_and_advances_read() {
use crate::bidding::american::{set_direct_dont, set_landy, set_unusual_notrump_defense};
set_landy(None);
set_unusual_notrump_defense(None);
set_direct_dont(true);
let x = read(&[bid(1, Strain::Notrump), Call::Double, Call::Pass]);
assert_eq!(x.partner().length(Suit::Spades), Range::new(0, 3));
assert_eq!(x.partner().points, Range::new(8, 37));
let relay = read(&[
bid(1, Strain::Notrump),
Call::Double,
Call::Pass,
bid(2, Strain::Clubs),
Call::Pass,
]);
assert_eq!(relay.partner().length(Suit::Clubs), Range::FULL_LENGTH);
let two_c = read(&[bid(1, Strain::Notrump), bid(2, Strain::Clubs), Call::Pass]);
assert_eq!(two_c.partner().length(Suit::Clubs), Range::new(4, 13));
assert_eq!(two_c.partner().points, Range::new(8, 37));
let pref = read(&[
bid(1, Strain::Notrump),
bid(2, Strain::Clubs),
Call::Pass,
bid(2, Strain::Diamonds),
Call::Pass,
]);
assert_eq!(pref.partner().length(Suit::Diamonds), Range::FULL_LENGTH);
let two_h = read(&[bid(1, Strain::Notrump), bid(2, Strain::Hearts), Call::Pass]);
assert_eq!(two_h.partner().length(Suit::Hearts), Range::new(4, 13));
assert_eq!(two_h.partner().length(Suit::Spades), Range::new(4, 13));
set_direct_dont(false);
let off = read(&[bid(1, Strain::Notrump), bid(2, Strain::Clubs), Call::Pass]);
assert_eq!(off.partner().length(Suit::Clubs), Range::new(5, 13));
}
#[test]
fn meckwell_overcalls_and_advances_read() {
use crate::bidding::american::{set_landy, set_meckwell, set_unusual_notrump_defense};
set_landy(None);
set_unusual_notrump_defense(None);
set_meckwell(true);
let x = read(&[bid(1, Strain::Notrump), Call::Double, Call::Pass]);
assert_eq!(x.partner().points, Range::new(8, 37));
assert_eq!(x.partner().length(Suit::Spades), Range::FULL_LENGTH);
assert_eq!(x.partner().length(Suit::Hearts), Range::FULL_LENGTH);
let relay = read(&[
bid(1, Strain::Notrump),
Call::Double,
Call::Pass,
bid(2, Strain::Clubs),
Call::Pass,
]);
assert_eq!(relay.partner().length(Suit::Clubs), Range::FULL_LENGTH);
let two_c = read(&[bid(1, Strain::Notrump), bid(2, Strain::Clubs), Call::Pass]);
assert_eq!(two_c.partner().length(Suit::Clubs), Range::new(4, 13));
assert_eq!(two_c.partner().points, Range::new(8, 37));
let two_d = read(&[
bid(1, Strain::Notrump),
bid(2, Strain::Diamonds),
Call::Pass,
]);
assert_eq!(two_d.partner().length(Suit::Diamonds), Range::new(4, 13));
let two_h = read(&[bid(1, Strain::Notrump), bid(2, Strain::Hearts), Call::Pass]);
assert_eq!(
two_h.partner().length(Suit::Spades).min,
0,
"natural 2♥ shows no spades",
);
set_meckwell(false);
let off = read(&[bid(1, Strain::Notrump), bid(2, Strain::Clubs), Call::Pass]);
assert_eq!(off.partner().length(Suit::Clubs), Range::new(5, 13));
set_unusual_notrump_defense(Some((8, 13)));
}
#[test]
fn narrowed_points_intersects_one_player() {
let inf = read(&[bid(1, Strain::Notrump)]);
assert_eq!(inf.rho().points, Range::new(14, 19));
let upper = inf.narrowed_points(Relative::Rho, Range::new(17, 19));
assert_eq!(
upper.rho().points,
Range::new(17, 19),
"narrowed to the half"
);
assert_eq!(inf.rho().points, Range::new(14, 19), "original unchanged");
assert_eq!(
upper.rho().length(Suit::Spades),
inf.rho().length(Suit::Spades)
);
assert_eq!(upper.partner().points, inf.partner().points);
let clamped = inf.narrowed_points(Relative::Rho, Range::new(0, POINTS_CAP));
assert_eq!(clamped.rho().points, Range::new(14, 19));
}
#[test]
fn third_seat_openings_are_light() {
let third = read(&[Call::Pass, Call::Pass, bid(1, Strain::Spades)]);
assert_eq!(third.rho().points, Range::new(9, 21));
}
#[test]
fn responses_narrow_partner_and_opener() {
let auction = [
bid(1, Strain::Hearts),
Call::Pass,
bid(2, Strain::Clubs),
Call::Pass,
];
let inf = read(&auction);
assert_eq!(inf.me().length(Suit::Hearts), Range::new(5, 13));
assert_eq!(inf.partner().length(Suit::Clubs), Range::new(4, 13));
assert_eq!(inf.partner().points, Range::new(13, 37));
}
#[test]
fn opener_rebid_shows_sixth_card() {
let auction = [
bid(1, Strain::Hearts),
Call::Pass,
bid(1, Strain::Spades),
Call::Pass,
bid(2, Strain::Hearts),
Call::Pass,
];
let inf = read(&auction);
assert_eq!(inf.partner().length(Suit::Hearts), Range::new(6, 13));
assert_eq!(inf.me().length(Suit::Spades), Range::new(4, 13));
assert_eq!(inf.me().points, Range::new(6, 37));
}
#[test]
fn competitive_opener_rebid_shows_sixth_card() {
let auction = [
bid(1, Strain::Diamonds),
bid(1, Strain::Hearts),
Call::Pass,
bid(2, Strain::Hearts),
bid(3, Strain::Diamonds),
Call::Pass,
];
let inf = read(&auction);
assert_eq!(inf.partner().length(Suit::Diamonds), Range::new(6, 13));
}
#[test]
fn overcall_shows_five_cards() {
let auction = [bid(1, Strain::Diamonds), bid(1, Strain::Spades)];
let inf = read(&auction);
assert_eq!(inf.partner().length(Suit::Diamonds), Range::new(3, 13));
assert_eq!(inf.rho().length(Suit::Spades), Range::new(5, 13));
assert_eq!(inf.rho().points, Range::new(8, 37));
}
#[test]
fn transfers_are_not_read_as_natural() {
let auction = [
bid(1, Strain::Notrump),
Call::Pass,
bid(2, Strain::Diamonds),
Call::Pass,
];
let inf = read(&auction);
assert_eq!(inf.partner().length(Suit::Diamonds), Range::FULL_LENGTH);
}
#[test]
fn three_level_suit_over_one_notrump_is_natural() {
let auction = [
bid(1, Strain::Notrump),
Call::Pass,
bid(3, Strain::Hearts),
Call::Pass,
];
let inf = read(&auction);
assert_eq!(inf.partner().length(Suit::Hearts), Range::new(5, 13));
}
#[test]
fn systems_on_overcall_transfer_is_not_read_as_diamonds() {
let auction = [
bid(1, Strain::Diamonds),
bid(1, Strain::Notrump),
Call::Pass,
bid(2, Strain::Diamonds),
Call::Pass,
];
let inf = read(&auction);
assert_eq!(inf.partner().length(Suit::Diamonds), Range::FULL_LENGTH);
}
#[test]
fn gladiator_cue_is_not_read_as_their_major() {
crate::bidding::american::set_nt_overcall_gladiator(true);
let auction = [
bid(1, Strain::Spades),
bid(1, Strain::Notrump),
Call::Pass,
bid(2, Strain::Spades),
Call::Pass,
];
let inf = read(&auction);
crate::bidding::american::set_nt_overcall_gladiator(false);
assert_eq!(inf.partner().length(Suit::Spades), Range::FULL_LENGTH);
assert_eq!(inf.partner().length(Suit::Hearts), Range::new(4, 13));
}
#[test]
fn gladiator_relay_is_not_read_as_clubs() {
crate::bidding::american::set_nt_overcall_gladiator(true);
let auction = [
bid(1, Strain::Spades),
bid(1, Strain::Notrump),
Call::Pass,
bid(2, Strain::Clubs),
Call::Pass,
];
let inf = read(&auction);
crate::bidding::american::set_nt_overcall_gladiator(false);
assert_eq!(inf.partner().length(Suit::Clubs), Range::FULL_LENGTH);
}
#[test]
fn completed_major_transfer_shows_five() {
let auction = [
bid(1, Strain::Notrump),
Call::Pass,
bid(2, Strain::Diamonds),
Call::Pass,
bid(2, Strain::Hearts),
Call::Pass,
];
let inf = read_booked(&auction);
assert_eq!(inf.me().length(Suit::Hearts), Range::new(5, 13));
assert_eq!(inf.me().length(Suit::Diamonds), Range::FULL_LENGTH);
}
#[test]
fn transfer_jump_to_game_shows_at_least_five() {
let auction = [
bid(1, Strain::Notrump),
Call::Pass,
bid(2, Strain::Diamonds),
Call::Pass,
bid(2, Strain::Hearts),
Call::Pass,
bid(4, Strain::Hearts),
Call::Pass,
];
let inf = read_booked(&auction);
assert_eq!(inf.partner().length(Suit::Hearts), Range::new(5, 13));
}
#[test]
fn transfer_then_three_major_shows_at_least_five() {
let auction = [
bid(1, Strain::Notrump),
Call::Pass,
bid(2, Strain::Diamonds),
Call::Pass,
bid(2, Strain::Hearts),
Call::Pass,
bid(3, Strain::Hearts),
Call::Pass,
];
let inf = read_booked(&auction);
assert!(inf.partner().length(Suit::Hearts).min >= 5);
}
#[test]
fn transfer_projection_covers_spades_and_two_notrump() {
let spades = read_booked(&[
bid(1, Strain::Notrump),
Call::Pass,
bid(2, Strain::Hearts),
Call::Pass,
bid(2, Strain::Spades),
Call::Pass,
bid(4, Strain::Spades),
Call::Pass,
]);
assert_eq!(spades.partner().length(Suit::Spades), Range::new(5, 13));
let two_nt = read_booked(&[
bid(2, Strain::Notrump),
Call::Pass,
bid(3, Strain::Diamonds),
Call::Pass,
bid(3, Strain::Hearts),
Call::Pass,
bid(4, Strain::Hearts),
Call::Pass,
]);
assert_eq!(two_nt.partner().length(Suit::Hearts), Range::new(5, 13));
}
#[test]
fn contested_transfer_auction_is_not_specially_read() {
let auction = [
bid(1, Strain::Notrump),
bid(2, Strain::Clubs),
bid(2, Strain::Diamonds),
Call::Pass,
bid(2, Strain::Hearts),
Call::Pass,
bid(4, Strain::Hearts),
Call::Pass,
];
let inf = read(&auction);
assert!(inf.partner().length(Suit::Hearts).min < 6);
}
#[test]
fn contested_transfer_lebensohl_reads_the_target_under_intervention() {
let auction = [
bid(1, Strain::Notrump),
bid(2, Strain::Spades),
bid(3, Strain::Diamonds),
bid(3, Strain::Spades),
];
let inf = read_booked(&auction);
assert!(
inf.partner().length(Suit::Hearts).min >= 5,
"transfer target pinned"
);
assert!(
inf.partner().length(Suit::Diamonds).min < 5,
"phantom suit not read"
);
}
#[test]
fn fallback_projection_decodes_contested_leaping_michaels() {
let auction = [
bid(1, Strain::Notrump),
bid(2, Strain::Diamonds),
bid(4, Strain::Diamonds),
Call::Pass,
];
let inf = read_booked(&auction);
assert!(
inf.partner().length(Suit::Hearts).min >= 5
&& inf.partner().length(Suit::Spades).min >= 5,
"fallback projection pins both majors for contested Leaping Michaels"
);
}
#[test]
fn contested_transfer_lebensohl_direct_jacoby_over_2d() {
let auction = [
bid(1, Strain::Notrump),
bid(2, Strain::Diamonds),
bid(3, Strain::Diamonds),
Call::Double,
];
let inf = read_booked(&auction);
assert!(inf.partner().length(Suit::Hearts).min >= 5);
}
#[test]
fn contested_transfer_lebensohl_cue_is_not_a_transfer() {
let auction = [
bid(1, Strain::Notrump),
bid(2, Strain::Spades),
bid(3, Strain::Spades),
Call::Pass,
];
let inf = read_booked(&auction);
assert!(inf.partner().length(Suit::Hearts).min < 5);
assert!(inf.partner().length(Suit::Spades).min < 5);
}
#[test]
fn relative_seat_tracks_the_actor() {
assert_eq!(
read(&[bid(1, Strain::Hearts)]).rho().points,
Range::new(10, 21)
);
assert_eq!(
read(&[bid(1, Strain::Hearts), Call::Pass]).partner().points,
Range::new(10, 21)
);
}
#[test]
fn limited_notrump_rebids_narrow_strength() {
let one_nt = read(&[
bid(1, Strain::Diamonds),
Call::Pass,
bid(1, Strain::Hearts),
Call::Pass,
bid(1, Strain::Notrump),
Call::Pass,
]);
assert_eq!(one_nt.partner().points, Range::new(12, 16));
let two_nt = read(&[
bid(1, Strain::Diamonds),
Call::Pass,
bid(1, Strain::Hearts),
Call::Pass,
bid(2, Strain::Notrump),
Call::Pass,
]);
assert_eq!(two_nt.partner().points, Range::new(18, 21));
}
#[test]
fn cheapest_two_notrump_over_a_response_is_not_strong() {
let inf = read(&[
bid(1, Strain::Diamonds),
Call::Pass,
bid(2, Strain::Clubs),
Call::Pass,
bid(2, Strain::Notrump),
Call::Pass,
]);
assert_eq!(inf.partner().points, Range::new(10, 21));
}
#[test]
fn raises_and_one_notrump_response_narrow_the_responder() {
let single = read(&[
bid(1, Strain::Hearts),
Call::Pass,
bid(2, Strain::Hearts),
Call::Pass,
]);
assert_eq!(single.partner().points, Range::new(6, 10));
let limit = read(&[
bid(1, Strain::Hearts),
Call::Pass,
bid(3, Strain::Hearts),
Call::Pass,
]);
assert_eq!(limit.partner().points, Range::new(10, 12));
let one_nt = read(&[
bid(1, Strain::Hearts),
Call::Pass,
bid(1, Strain::Notrump),
Call::Pass,
]);
assert_eq!(one_nt.partner().points, Range::new(6, 12));
}
#[test]
fn competition_suppresses_the_limited_rebid_reading() {
let inf = read(&[
bid(1, Strain::Diamonds),
Call::Pass,
bid(1, Strain::Hearts),
bid(1, Strain::Spades),
bid(1, Strain::Notrump),
Call::Pass,
]);
assert_eq!(inf.partner().points, Range::new(10, 21));
}
#[test]
fn rubens_cue_raise_shows_support() {
let inf = read(&[
bid(1, Strain::Spades),
bid(2, Strain::Clubs),
Call::Pass,
bid(2, Strain::Spades),
Call::Pass,
]);
assert!(inf.partner().length(Suit::Clubs).min >= 3);
assert!(inf.partner().points.min >= 10);
assert_eq!(inf.partner().length(Suit::Spades), Range::FULL_LENGTH);
}
#[test]
fn rubens_transfer_is_not_read_as_natural() {
let inf = read(&[
bid(1, Strain::Clubs),
bid(1, Strain::Spades),
Call::Pass,
bid(2, Strain::Clubs),
Call::Pass,
]);
assert_eq!(inf.partner().length(Suit::Clubs), Range::FULL_LENGTH);
}
#[test]
fn rubens_reading_respects_the_knob() {
crate::bidding::instinct::set_rubens_advances(false);
let inf = read(&[
bid(1, Strain::Clubs),
bid(1, Strain::Spades),
Call::Pass,
bid(2, Strain::Clubs),
Call::Pass,
]);
assert!(inf.partner().length(Suit::Clubs).min >= 4);
crate::bidding::instinct::set_rubens_advances(true);
}
#[test]
fn rubens_limit_raise_transfer_records_support() {
let inf = read(&[
bid(1, Strain::Clubs),
bid(1, Strain::Spades),
Call::Pass,
bid(2, Strain::Hearts),
Call::Pass,
]);
assert!(inf.partner().length(Suit::Spades).min >= 3);
assert!(inf.partner().points.min >= 10);
assert_eq!(inf.partner().length(Suit::Hearts), Range::FULL_LENGTH);
}
#[test]
fn rubens_new_suit_transfer_records_the_target() {
let inf = read(&[
bid(1, Strain::Clubs),
bid(1, Strain::Spades),
Call::Pass,
bid(2, Strain::Clubs),
Call::Pass,
]);
assert!(inf.partner().length(Suit::Diamonds).min >= 5);
assert!(inf.partner().points.min >= 10);
assert_eq!(inf.partner().length(Suit::Clubs), Range::FULL_LENGTH);
}
#[test]
fn rubens_transfer_records_despite_intervention() {
let inf = read(&[
bid(1, Strain::Clubs),
bid(1, Strain::Spades),
Call::Pass,
bid(2, Strain::Hearts),
Call::Double,
]);
assert!(inf.partner().length(Suit::Spades).min >= 3);
assert!(inf.partner().points.min >= 10);
}
#[test]
fn rubens_transfer_is_not_read_for_the_opponents() {
let inf = read(&[
bid(1, Strain::Clubs),
bid(1, Strain::Spades),
Call::Pass,
bid(2, Strain::Hearts),
Call::Pass,
Call::Pass,
]);
assert_eq!(inf.lho().length(Suit::Spades), Range::FULL_LENGTH);
assert_eq!(inf.lho().points, Range::FULL_POINTS);
}
#[test]
fn michaels_cue_over_our_major_reads_the_other_major() {
use crate::bidding::american::set_uvu_over_majors;
set_uvu_over_majors(true);
let inf = read(&[bid(1, Strain::Hearts), bid(2, Strain::Hearts)]);
assert!(inf.rho().length(Suit::Spades).min >= 5, "the shown major");
assert_eq!(
inf.rho().length(Suit::Hearts),
Range::FULL_LENGTH,
"the cue is not natural hearts"
);
set_uvu_over_majors(false);
let inf = read(&[bid(1, Strain::Hearts), bid(2, Strain::Hearts)]);
assert!(inf.rho().length(Suit::Hearts).min >= 5);
assert_eq!(inf.rho().length(Suit::Spades), Range::FULL_LENGTH);
set_uvu_over_majors(true);
}
#[test]
fn unusual_2nt_over_our_major_reads_both_minors() {
use crate::bidding::american::set_uvu_over_majors;
set_uvu_over_majors(true);
let inf = read(&[bid(1, Strain::Spades), bid(2, Strain::Notrump)]);
assert!(inf.rho().length(Suit::Clubs).min >= 5);
assert!(inf.rho().length(Suit::Diamonds).min >= 5);
set_uvu_over_majors(false);
let inf = read(&[bid(1, Strain::Spades), bid(2, Strain::Notrump)]);
assert_eq!(inf.rho().length(Suit::Clubs), Range::FULL_LENGTH);
assert_eq!(inf.rho().length(Suit::Diamonds), Range::FULL_LENGTH);
set_uvu_over_majors(true);
}
#[test]
fn uvu_major_cue_projects_the_raise() {
use crate::bidding::american::set_uvu_over_majors;
set_uvu_over_majors(true);
let inf = read_booked(&[
bid(1, Strain::Hearts),
bid(2, Strain::Notrump),
bid(3, Strain::Clubs),
Call::Pass,
]);
let cue_bidder = inf.partner();
assert!(
cue_bidder.length(Suit::Hearts).min >= 3,
"the projected fit"
);
assert!(cue_bidder.points.min >= 10, "the projected strength");
assert_eq!(
cue_bidder.length(Suit::Clubs),
Range::FULL_LENGTH,
"not natural clubs"
);
}
#[test]
fn rubens_transfer_reading_knob_recovers_suppress_only() {
set_rubens_transfer_reading(false);
let inf = read(&[
bid(1, Strain::Clubs),
bid(1, Strain::Spades),
Call::Pass,
bid(2, Strain::Hearts),
Call::Pass,
]);
assert_eq!(inf.partner().length(Suit::Spades), Range::FULL_LENGTH);
assert_eq!(inf.partner().length(Suit::Hearts), Range::FULL_LENGTH);
assert_eq!(inf.partner().points, Range::FULL_POINTS);
set_rubens_transfer_reading(true);
}
#[test]
fn range_intersect_widens_on_conflict() {
assert_eq!(
Range::new(5, 13).intersect(Range::new(6, 13)),
Range::new(6, 13)
);
assert_eq!(
Range::new(0, 3).intersect(Range::new(6, 13)),
Range::new(0, 13)
);
}
#[test]
fn artificial_calls_are_alerted() {
use crate::bidding::american::american;
let pair = american();
let tries = [
("constructive", &pair.constructive.0),
("competitive", &pair.competitive.0),
("defensive", &pair.defensive.0),
];
let mut worklist: Vec<String> = Vec::new();
for (phase, trie) in tries {
for (auction, classifier) in trie {
let auction: &[Call] = &auction;
let Some(rules) = classifier.as_rules() else {
continue;
};
let context = Context::new(RelativeVulnerability::NONE, auction)
.with_prefixes(trie.common_prefixes(auction));
for rule in rules.rules() {
let made = rule.call();
if super::artificial(&rule.project(&context), made) && rule.alert().is_none() {
worklist.push(format!(
"{phase}: [{}] {made} (label: {:?})",
auction
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(" "),
rule.label(),
));
}
}
}
}
worklist.sort();
worklist.dedup();
assert!(
worklist.is_empty(),
"{} artificial calls lack an alert (the retirement worklist):\n{}",
worklist.len(),
worklist.join("\n"),
);
}
#[test]
fn gladiator_artificial_calls_are_alerted() {
use crate::bidding::american::{american, set_nt_overcall_gladiator};
set_nt_overcall_gladiator(true);
let pair = american();
set_nt_overcall_gladiator(false);
let trie = &pair.defensive.0;
let mut worklist: Vec<String> = Vec::new();
for (auction, classifier) in trie {
let auction: &[Call] = &auction;
let Some(rules) = classifier.as_rules() else {
continue;
};
let context = Context::new(RelativeVulnerability::NONE, auction)
.with_prefixes(trie.common_prefixes(auction));
for rule in rules.rules() {
let made = rule.call();
if super::artificial(&rule.project(&context), made) && rule.alert().is_none() {
worklist.push(format!(
"[{}] {made} (label: {:?})",
auction
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(" "),
rule.label(),
));
}
}
}
worklist.sort();
worklist.dedup();
assert!(
worklist.is_empty(),
"{} Gladiator artificial calls lack an alert:\n{}",
worklist.len(),
worklist.join("\n"),
);
}
proptest! {
#[test]
fn opening_inference_contains_the_opener(seed in any::<u64>()) {
use crate::bidding::trie::Classifier;
use crate::bidding::american::openings;
use contract_bridge::deck::full_deal;
use rand::SeedableRng;
let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
let deal = full_deal(&mut rng);
let hand: Hand = deal[contract_bridge::Seat::North];
let context = Context::new(RelativeVulnerability::NONE, &[]);
let logits = openings().classify(hand, &context);
let Some((call, _)) = (&logits.0)
.into_iter()
.filter(|(_, l)| l.is_finite())
.max_by(|(_, a), (_, b)| a.partial_cmp(b).expect("not NaN"))
else {
return Ok(());
};
let Call::Bid(_) = call else { return Ok(()); };
let inf = read(&[call]);
let opener = inf.rho();
let points = point_count(hand);
prop_assert!(
opener.points.contains(points),
"{call} opener with {points} points outside {:?}",
opener.points
);
for suit in Suit::ASC {
let length = hand[suit].len();
#[allow(clippy::cast_possible_truncation)]
let length = length as u8;
prop_assert!(
opener.length(suit).contains(length),
"{call} opener with {length} {suit:?} outside {:?}",
opener.length(suit)
);
}
}
}
}