use super::fallback::{Always, Fallback, Guard};
use super::instinct::instinct;
use super::trie::Classifier;
use super::{Constructive, Family, Pair, Trie};
use contract_bridge::auction::Call;
use contract_bridge::{Bid, Strain};
use std::sync::Arc;
mod btu_notrump;
mod competition;
mod defense;
mod game_force;
mod notrump;
mod openings;
mod raises;
mod rebids;
mod responses;
mod rubens;
mod slam;
mod stenberg;
mod strong_two;
mod weak_twos;
pub use competition::competition;
pub use defense::{advance_double, defense_to_suit, defense_to_weak_two};
pub use notrump::notrump_responses;
pub use openings::openings;
pub use responses::{major_responses, minor_responses};
const fn call(level: u8, strain: Strain) -> Call {
Call::Bid(Bid::new(level, strain))
}
fn insert_all_seats(
book: &mut Trie,
suffix: &[Call],
max_passes: usize,
rules: impl Classifier + 'static,
) {
let shared: Arc<dyn Classifier> = Arc::new(rules);
for n in 0..=max_passes {
let key: Vec<Call> = core::iter::repeat_n(Call::Pass, n)
.chain(suffix.iter().copied())
.collect();
book.insert_arc(&key, Arc::clone(&shared));
}
}
fn uncontested(our_calls: &[Call]) -> Vec<Call> {
our_calls
.iter()
.flat_map(|&call| [call, Call::Pass])
.collect()
}
fn insert_uncontested(book: &mut Trie, our_calls: &[Call], rules: impl Classifier + 'static) {
insert_all_seats(book, &uncontested(our_calls), 3, rules);
}
fn fallback_all_seats(
book: &mut Trie,
suffix: &[Call],
max_passes: usize,
guard: Arc<dyn Guard>,
fallback: Fallback,
) {
for n in 0..=max_passes {
let key: Vec<Call> = core::iter::repeat_n(Call::Pass, n)
.chain(suffix.iter().copied())
.collect();
book.fallback_arc_at(&key, Arc::clone(&guard), fallback.clone());
}
}
#[must_use]
pub fn two_over_one() -> Pair {
with_instinct_floor(bare_two_over_one())
}
fn with_instinct_floor(mut pair: Pair) -> Pair {
let floor = Fallback::classify(instinct());
pair.competitive.fallback_at(&[], Always, floor.clone());
pair.defensive.fallback_at(&[], Always, floor);
pair
}
#[must_use]
pub fn bare_two_over_one() -> Pair {
let mut c = Constructive::new();
openings::register(&mut c);
responses::register(&mut c);
notrump::register(&mut c);
rebids::register(&mut c);
game_force::register(&mut c);
raises::register(&mut c);
strong_two::register(&mut c);
weak_twos::register(&mut c);
Pair::new(
Family::NATURAL,
c,
competition::competition(),
defense::defensive(),
)
}
#[must_use]
pub fn bare_two_over_one_strawberry() -> Pair {
let mut c = Constructive::new();
openings::register(&mut c);
responses::register(&mut c);
notrump::register_two_nt_and_rebids(&mut c);
btu_notrump::register(&mut c);
rebids::register(&mut c);
game_force::register(&mut c);
stenberg::register(&mut c);
strong_two::register(&mut c);
weak_twos::register(&mut c);
let mut defensive = defense::defensive();
rubens::register(&mut defensive);
Pair::new(Family::NATURAL, c, competition::competition(), defensive)
}
#[must_use]
pub fn two_over_one_strawberry() -> Pair {
let mut pair = with_instinct_floor(bare_two_over_one_strawberry());
let floor = Fallback::classify(instinct());
pair.constructive.fallback_at(&[], Always, floor);
pair
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bidding::Rules;
use crate::bidding::context::Context;
use contract_bridge::auction::RelativeVulnerability;
use contract_bridge::{Hand, Suit};
fn best(rules: &Rules, auction: &[Call], hand: &str) -> Call {
let hand: Hand = hand.parse().expect("valid test hand");
let context = Context::new(RelativeVulnerability::NONE, auction);
let logits = rules.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")
}
#[test]
fn openings_pick_the_descriptive_bid() {
let o = openings();
assert_eq!(best(&o, &[], "AQ32.K53.QJ4.A92"), call(1, Strain::Notrump));
assert_eq!(best(&o, &[], "AKQ2.AKJ.KQ4.932"), call(2, Strain::Clubs));
assert_eq!(best(&o, &[], "A2.KQJ53.Q42.J92"), call(1, Strain::Hearts));
assert_eq!(best(&o, &[], "KQJ732.53.842.92"), call(2, Strain::Spades));
}
#[test]
fn openings_suppress_weak_twos_in_fourth_seat() {
let o = openings();
assert_eq!(best(&o, &[], "KQJ732.53.842.92"), call(2, Strain::Spades));
assert_eq!(best(&o, &[Call::Pass; 3], "KQJ732.53.842.92"), Call::Pass,);
}
#[test]
fn major_responses_run_the_2_over_1_ladder() {
let r = major_responses(Suit::Hearts);
let a = [call(1, Strain::Hearts), Call::Pass];
assert_eq!(best(&r, &a, "K2.KQ54.A964.Q92"), call(2, Strain::Notrump));
assert_eq!(best(&r, &a, "Q32.J53.A964.Q92"), call(2, Strain::Hearts));
assert_eq!(best(&r, &a, "A2.K3.Q543.KJ85"), call(2, Strain::Clubs));
}
#[test]
fn notrump_responses_transfer_and_stayman() {
let r = notrump_responses();
let a = [call(1, Strain::Notrump), Call::Pass];
assert_eq!(best(&r, &a, "KJ542.Q32.K43.92"), call(2, Strain::Hearts));
assert_eq!(best(&r, &a, "KJ54.Q32.K43.Q92"), call(2, Strain::Clubs));
}
#[test]
fn defense_doubles_with_strength() {
let r = defense_to_suit(Bid::new(1, Strain::Diamonds));
let a = [call(1, Strain::Diamonds)];
assert_eq!(best(&r, &a, "A.Q6.KJ852.AKJ42"), Call::Double);
assert_eq!(best(&r, &a, "AQJ32.853.42.K92"), call(1, Strain::Spades));
}
fn play_uncontested(opener: &str, responder: &str) -> Vec<Call> {
use crate::bidding::{Family, System};
let stance = super::two_over_one_strawberry().against(Family::NATURAL);
let oh: Hand = opener.parse().expect("valid opener hand");
let rh: Hand = responder.parse().expect("valid responder hand");
let mut auction = vec![call(1, Strain::Notrump), Call::Pass];
loop {
let n = auction.len();
if n >= 4 && auction[n - 3..].iter().all(|&c| c == Call::Pass) {
break;
}
assert!(n <= 48, "auction did not terminate: {auction:?}");
let next = match n % 4 {
seat @ (0 | 2) => {
let hand = if seat == 0 { oh } else { rh };
match stance.classify(hand, RelativeVulnerability::NONE, &auction) {
None => Call::Pass,
Some(logits) => (&logits.0)
.into_iter()
.filter(|(_, l)| l.is_finite())
.max_by(|(_, a), (_, b)| a.partial_cmp(b).expect("not NaN"))
.map(|(c, _)| c)
.unwrap_or(Call::Pass),
}
}
_ => Call::Pass,
};
auction.push(next);
}
auction
}
fn final_bid(auction: &[Call]) -> Bid {
auction
.iter()
.rev()
.find_map(|c| match c {
Call::Bid(b) => Some(*b),
_ => None,
})
.expect("some contract was reached")
}
#[test]
fn strawberry_btu_gf_auctions_reach_game() {
let opener = "AQ32.KJ5.KQ4.Q92";
let gf_hands = [
"KQ542.AJ842.K.32", "AJ52.Q73.AJ54.32", "73.AKQ842.K64.53", "KQ52.AQ984.J6.32", "K92.Q73.AQ54.Q32", ];
for rh in gf_hands {
let auction = play_uncontested(opener, rh);
let bid = final_bid(&auction);
let reached_game =
bid.level.get() >= 4 || (bid.level.get() == 3 && bid.strain == Strain::Notrump);
assert!(
reached_game,
"GF responder {rh} stranded below game: {auction:?} (final {bid:?})"
);
}
}
}