use super::{call, insert_uncontested};
use crate::bidding::constraint::{
balanced, fifths, hcp, len, partner_suit_is, points, stopper_in, support,
};
use crate::bidding::{Alert, Rules, Trie};
use contract_bridge::auction::Call;
use contract_bridge::{Bid, Strain, Suit};
use std::cell::Cell;
std::thread_local! {
static MECKSTROTH: Cell<bool> = const { Cell::new(true) };
}
pub fn set_meckstroth_adjunct(on: bool) {
MECKSTROTH.with(|cell| cell.set(on));
}
fn meckstroth() -> bool {
MECKSTROTH.with(Cell::get)
}
std::thread_local! {
static BALANCED_1NT_REBID: Cell<bool> = const { Cell::new(true) };
}
pub fn set_balanced_1nt_rebid(on: bool) {
BALANCED_1NT_REBID.with(|cell| cell.set(on));
}
fn balanced_1nt_rebid() -> bool {
BALANCED_1NT_REBID.with(Cell::get)
}
std::thread_local! {
static OPENER_EXTRAS_LADDER: Cell<bool> = const { Cell::new(true) };
}
pub fn set_opener_extras_ladder(on: bool) {
OPENER_EXTRAS_LADDER.with(|cell| cell.set(on));
}
pub(crate) fn opener_extras_ladder() -> bool {
OPENER_EXTRAS_LADDER.with(Cell::get)
}
std::thread_local! {
static OPENER_MAJOR_JUMP_REBID: Cell<bool> = const { Cell::new(true) };
}
pub fn set_opener_major_jump_rebid(on: bool) {
OPENER_MAJOR_JUMP_REBID.with(|cell| cell.set(on));
}
pub(crate) fn opener_major_jump_rebid() -> bool {
OPENER_MAJOR_JUMP_REBID.with(Cell::get)
}
fn with_major_jump_rebid(rules: Rules, major: Suit, highest: Bid) -> Rules {
if !opener_major_jump_rebid() {
return rules;
}
let trump = Strain::from(major);
let level = cheapest_level_over(highest, trump) + 1;
rules.rule(Bid::new(level, trump), 1.5, len(major, 6..) & points(16..))
}
fn cheapest_level_over(highest: Bid, strain: Strain) -> u8 {
if strain > highest.strain {
highest.level.get()
} else {
highest.level.get() + 1
}
}
const OPENER_REVERSE: Alert = Alert("opener-reverse");
const OPENER_JUMP_SHIFT: Alert = Alert("opener-jump-shift");
fn with_extras_ladder(
mut rules: Rules,
opener: Suit,
highest: Bid,
responder: Option<Suit>,
) -> Rules {
if !opener_extras_ladder() {
return rules;
}
let opener_strain = Strain::from(opener);
let jump_rebid_level = cheapest_level_over(highest, opener_strain) + 1;
if jump_rebid_level <= 3 {
rules = rules.rule(
Bid::new(jump_rebid_level, opener_strain),
1.5,
len(opener, 6..) & points(16..),
);
}
for second in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
if second == opener || responder == Some(second) {
continue;
}
let second_strain = Strain::from(second);
let cheapest = cheapest_level_over(highest, second_strain);
if cheapest == 2 && second_strain > opener_strain {
rules = rules
.rule(
Bid::new(2, second_strain),
1.6,
len(opener, 5..) & len(second, 4..) & points(17..),
)
.alert(OPENER_REVERSE);
}
let jump_shift_level = cheapest + 1;
if jump_shift_level <= 3 {
rules = rules
.rule(
Bid::new(jump_shift_level, second_strain),
1.7,
len(opener, 5..) & len(second, 4..) & points(18..),
)
.alert(OPENER_JUMP_SHIFT);
}
}
rules
}
fn is_invitational_minor_jump(rebid: Call) -> bool {
rebid == call(3, Strain::Clubs) || rebid == call(3, Strain::Diamonds)
}
fn with_invitational_minors(mut rules: Rules) -> Rules {
if meckstroth() {
for minor in [Suit::Clubs, Suit::Diamonds] {
rules = rules.rule(
Bid::new(3, Strain::from(minor)),
1.05,
len(minor, 5..) & points(15..=17),
);
}
}
rules
}
fn rebid_one_heart_one_spade() -> Rules {
let mut rules = Rules::new()
.rule(
Bid::new(4, Strain::Spades),
2.6,
support(4..) & points(19..),
)
.rule(
Bid::new(3, Strain::Spades),
2.2,
support(4..) & points(16..=18),
)
.rule(
Bid::new(2, Strain::Spades),
1.8,
support(4..) & points(12..=15),
)
.rule(Bid::new(2, Strain::Hearts), 1.4, len(Suit::Hearts, 6..))
.rule(
Bid::new(2, Strain::Notrump),
1.2,
fifths(18.0..20.0) & balanced(),
);
rules = with_invitational_minors(rules);
rules = with_major_jump_rebid(rules, Suit::Hearts, Bid::new(1, Strain::Spades));
rules
.rule(Bid::new(2, Strain::Clubs), 0.9, len(Suit::Clubs, 4..))
.rule(Bid::new(2, Strain::Diamonds), 0.9, len(Suit::Diamonds, 4..))
.rule(Bid::new(1, Strain::Notrump), 0.5, fifths(12.0..15.0))
.rule(Bid::new(1, Strain::Notrump), 0.2, hcp(0..))
}
fn rebid_after_forcing_notrump(major: Suit) -> Rules {
let trump = Strain::from(major);
let mut rules = Rules::new()
.rule(
Bid::new(2, Strain::Notrump),
1.2,
fifths(18.0..20.0) & balanced(),
)
.rule(Bid::new(2, trump), 1.0, len(major, 6..));
rules = with_invitational_minors(rules);
rules = with_major_jump_rebid(rules, major, Bid::new(1, Strain::Notrump));
for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts] {
if Strain::from(suit) < trump {
rules = rules.rule(Bid::new(2, Strain::from(suit)), 0.9, len(suit, 4..));
}
}
rules.rule(Bid::new(2, trump), 0.3, len(major, 5..))
}
fn rebid_raise_major(responder_major: Suit, opener_minor: Suit) -> Rules {
let m = Strain::from(responder_major);
let mut rules = Rules::new()
.rule(Bid::new(4, m), 2.6, support(4..) & points(19..))
.rule(Bid::new(3, m), 2.2, support(4..) & points(16..=18))
.rule(Bid::new(2, m), 1.8, support(4..) & points(12..=15))
.rule(
Bid::new(2, Strain::Notrump),
1.2,
fifths(18.0..20.0) & balanced(),
);
if balanced_1nt_rebid() {
rules = rules.rule(
Bid::new(1, Strain::Notrump),
0.92,
fifths(12.0..15.0) & balanced(),
);
}
if responder_major == Suit::Hearts && super::responses::up_the_line() {
rules = rules.rule(Bid::new(1, Strain::Spades), 0.95, len(Suit::Spades, 4..));
}
rules = with_extras_ladder(rules, opener_minor, Bid::new(1, m), Some(responder_major));
rules
.rule(
Bid::new(2, Strain::from(opener_minor)),
0.9,
len(opener_minor, 5..),
)
.rule(
Bid::new(1, Strain::Notrump),
0.5,
fifths(12.0..15.0) & balanced(),
)
.rule(Bid::new(1, Strain::Notrump), 0.2, hcp(0..))
}
fn rebid_one_club_one_diamond() -> Rules {
let mut rules = Rules::new()
.rule(Bid::new(1, Strain::Hearts), 1.3, len(Suit::Hearts, 4..))
.rule(
Bid::new(1, Strain::Spades),
1.3,
len(Suit::Spades, 4..) & len(Suit::Hearts, ..4),
)
.rule(
Bid::new(3, Strain::Diamonds),
1.5,
support(4..) & points(16..=18),
)
.rule(
Bid::new(2, Strain::Diamonds),
1.2,
support(4..) & points(12..=15),
)
.rule(
Bid::new(2, Strain::Notrump),
1.1,
fifths(18.0..20.0) & balanced(),
);
if super::responses::up_the_line() {
rules = rules.rule(Bid::new(2, Strain::Clubs), 0.9, len(Suit::Clubs, 6..));
}
rules = with_extras_ladder(
rules,
Suit::Clubs,
Bid::new(1, Strain::Diamonds),
Some(Suit::Diamonds),
);
rules
.rule(
Bid::new(1, Strain::Notrump),
0.5,
fifths(12.0..15.0) & balanced(),
)
.rule(Bid::new(1, Strain::Notrump), 0.2, hcp(0..))
}
fn responder_after_forcing_notrump(major: Suit) -> Rules {
let trump = Strain::from(major);
let mut rules = Rules::new()
.rule(Bid::new(3, trump), 1.5, len(major, 3..) & hcp(10..=12))
.rule(Bid::new(2, Strain::Notrump), 1.2, hcp(11..=12))
.rule(Bid::new(2, trump), 1.0, len(major, 2..) & hcp(7..))
.rule(Call::Pass, 0.0, hcp(0..));
for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
if suit != major {
rules = rules.rule(
Bid::new(2, Strain::from(suit)),
1.1,
len(suit, 6..) & hcp(..=9),
);
}
}
rules
}
fn responder_after_invitational_minor(major: Suit) -> Rules {
let trump = Strain::from(major);
Rules::new()
.rule(Bid::new(4, trump), 1.4, len(major, 3..) & points(10..))
.rule(Bid::new(3, Strain::Notrump), 1.2, points(10..))
.rule(Bid::new(3, trump), 1.0, len(major, 2..) & points(..10))
.rule(Call::Pass, 0.0, points(0..))
}
fn responder_after_major_jump_rebid(major: Suit) -> Rules {
let trump = Strain::from(major);
Rules::new()
.rule(Bid::new(4, trump), 1.4, len(major, 2..) & points(8..))
.rule(Bid::new(3, Strain::Notrump), 1.2, points(9..))
.rule(Call::Pass, 0.0, points(0..))
}
fn opener_accept_notrump_invite() -> Rules {
Rules::new()
.rule(Bid::new(3, Strain::Notrump), 1.0, hcp(14..))
.rule(Call::Pass, 0.0, hcp(0..))
}
fn opener_accept_limit_raise(major: Suit) -> Rules {
Rules::new()
.rule(Bid::new(4, Strain::from(major)), 1.0, points(14..))
.rule(Call::Pass, 0.0, hcp(0..))
}
fn register_forcing_notrump_continuations(book: &mut Trie) {
for major in [Suit::Hearts, Suit::Spades] {
let one_major = call(1, Strain::from(major));
let one_nt = call(1, Strain::Notrump);
let mut seen: Vec<Call> = Vec::new();
for rule in rebid_after_forcing_notrump(major).rules() {
let rebid = rule.call();
if rebid != call(2, Strain::Notrump)
&& !is_invitational_minor_jump(rebid)
&& !seen.contains(&rebid)
{
seen.push(rebid);
}
}
for rebid in seen {
insert_uncontested(
book,
&[one_major, one_nt, rebid],
responder_after_forcing_notrump(major),
);
insert_uncontested(
book,
&[one_major, one_nt, rebid, call(2, Strain::Notrump)],
opener_accept_notrump_invite(),
);
insert_uncontested(
book,
&[one_major, one_nt, rebid, call(3, Strain::from(major))],
opener_accept_limit_raise(major),
);
}
}
register_invitational_minor_continuations(book);
}
fn register_invitational_minor_continuations(book: &mut Trie) {
if !meckstroth() {
return;
}
let three_minors = [call(3, Strain::Clubs), call(3, Strain::Diamonds)];
for major in [Suit::Hearts, Suit::Spades] {
let prefix = [call(1, Strain::from(major)), call(1, Strain::Notrump)];
for three_m in three_minors {
insert_uncontested(
book,
&[prefix[0], prefix[1], three_m],
responder_after_invitational_minor(major),
);
}
}
let one_heart = call(1, Strain::Hearts);
let one_spade = call(1, Strain::Spades);
for three_m in three_minors {
insert_uncontested(
book,
&[one_heart, one_spade, three_m],
responder_after_invitational_minor(Suit::Hearts),
);
}
}
fn register_major_jump_rebid_continuations(book: &mut Trie) {
if !opener_major_jump_rebid() {
return;
}
for major in [Suit::Hearts, Suit::Spades] {
insert_uncontested(
book,
&[
call(1, Strain::from(major)),
call(1, Strain::Notrump),
call(3, Strain::from(major)),
],
responder_after_major_jump_rebid(major),
);
}
insert_uncontested(
book,
&[
call(1, Strain::Hearts),
call(1, Strain::Spades),
call(3, Strain::Hearts),
],
responder_after_major_jump_rebid(Suit::Hearts),
);
}
std::thread_local! {
static MAJOR_REBID_TAILS: Cell<bool> = const { Cell::new(true) };
}
pub fn set_major_rebid_tails(on: bool) {
MAJOR_REBID_TAILS.with(|cell| cell.set(on));
}
fn major_rebid_tails() -> bool {
MAJOR_REBID_TAILS.with(Cell::get)
}
const FOURTH_SUIT: Alert = Alert("fourth-suit-forcing");
std::thread_local! {
static FOURTH_SUIT_FORCING: Cell<bool> = const { Cell::new(true) };
}
pub fn set_fourth_suit_forcing(on: bool) {
FOURTH_SUIT_FORCING.with(|cell| cell.set(on));
}
fn fourth_suit_forcing() -> bool {
FOURTH_SUIT_FORCING.with(Cell::get)
}
#[must_use]
fn responder_after_spade_raise() -> Rules {
Rules::new()
.rule(Bid::new(4, Strain::Notrump), 2.0, points(16..))
.alert(super::slam::RKCB)
.rule(Bid::new(4, Strain::Spades), 1.5, points(12..))
.rule(Bid::new(3, Strain::Spades), 1.2, points(10..=11))
.rule(Call::Pass, 0.0, hcp(0..))
}
#[must_use]
fn responder_after_spade_jump() -> Rules {
Rules::new()
.rule(Bid::new(4, Strain::Notrump), 1.5, points(14..))
.alert(super::slam::RKCB)
.rule(Bid::new(4, Strain::Spades), 1.0, points(8..))
.rule(Call::Pass, 0.0, hcp(0..))
}
#[must_use]
fn responder_after_heart_rebid() -> Rules {
Rules::new()
.rule(
Bid::new(4, Strain::Hearts),
1.5,
len(Suit::Hearts, 2..) & points(13..),
)
.rule(Bid::new(3, Strain::Notrump), 1.3, points(13..))
.rule(
Bid::new(3, Strain::Hearts),
1.2,
len(Suit::Hearts, 2..) & points(10..=12),
)
.rule(Bid::new(2, Strain::Notrump), 1.0, points(10..=12))
.rule(Call::Pass, 0.0, hcp(0..))
}
#[must_use]
fn opener_after_heart_invite() -> Rules {
Rules::new()
.rule(Bid::new(3, Strain::Notrump), 1.0, hcp(14..))
.rule(Bid::new(3, Strain::Hearts), 0.5, hcp(0..))
}
#[must_use]
fn responder_after_minor_rebid(minor: Suit) -> Rules {
let m = Strain::from(minor);
let mut rules = Rules::new();
if minor == Suit::Clubs && fourth_suit_forcing() {
rules = rules
.rule(Bid::new(2, Strain::Diamonds), 2.0, points(12..))
.alert(FOURTH_SUIT);
}
rules
.rule(
Bid::new(3, Strain::Hearts),
1.3,
len(Suit::Hearts, 3..) & points(10..=12),
)
.rule(Bid::new(3, m), 1.25, len(minor, 5..) & points(10..=12))
.rule(Bid::new(2, Strain::Notrump), 1.2, points(10..=12))
.rule(
Bid::new(2, Strain::Spades),
1.05,
len(Suit::Spades, 6..) & hcp(..=9),
)
.rule(
Bid::new(2, Strain::Hearts),
1.0,
len(Suit::Hearts, 2..) & hcp(6..=9),
)
.rule(Bid::new(3, Strain::Notrump), 0.9, hcp(13..))
.rule(Call::Pass, 0.0, hcp(0..))
}
#[must_use]
fn opener_accept_minor_raise() -> Rules {
Rules::new()
.rule(Bid::new(3, Strain::Notrump), 1.0, points(14..))
.rule(Call::Pass, 0.0, hcp(0..))
}
#[must_use]
fn opener_after_fourth_suit() -> Rules {
Rules::new()
.rule(Bid::new(2, Strain::Spades), 1.4, len(Suit::Spades, 3..))
.rule(Bid::new(2, Strain::Hearts), 1.3, len(Suit::Hearts, 6..))
.rule(
Bid::new(2, Strain::Notrump),
1.2,
stopper_in(Suit::Diamonds),
)
.rule(Bid::new(3, Strain::Clubs), 1.1, len(Suit::Clubs, 5..))
.rule(Bid::new(2, Strain::Hearts), 0.2, len(Suit::Hearts, 5..))
}
#[must_use]
fn responder_after_fourth_suit_answer() -> Rules {
Rules::new()
.rule(
Bid::new(4, Strain::Spades),
1.5,
partner_suit_is(Suit::Spades) & len(Suit::Spades, 5..),
)
.rule(Bid::new(4, Strain::Hearts), 1.2, len(Suit::Hearts, 3..))
.rule(
Bid::new(4, Strain::Hearts),
1.1,
partner_suit_is(Suit::Hearts) & len(Suit::Hearts, 2..),
)
.rule(Bid::new(3, Strain::Notrump), 0.8, hcp(0..))
}
fn register_major_rebid_tails(book: &mut Trie) {
if !major_rebid_tails() {
return;
}
let one_heart = call(1, Strain::Hearts);
let one_spade = call(1, Strain::Spades);
let two_spades = call(2, Strain::Spades);
let three_spades = call(3, Strain::Spades);
let two_hearts = call(2, Strain::Hearts);
let three_hearts = call(3, Strain::Hearts);
let two_nt = call(2, Strain::Notrump);
let after_two_spades = [one_heart, one_spade, two_spades];
insert_uncontested(book, &after_two_spades, responder_after_spade_raise());
insert_uncontested(
book,
&[one_heart, one_spade, two_spades, three_spades],
opener_accept_limit_raise(Suit::Spades),
);
super::slam::install_rkcb(book, &after_two_spades, Suit::Spades);
let after_three_spades = [one_heart, one_spade, three_spades];
insert_uncontested(book, &after_three_spades, responder_after_spade_jump());
super::slam::install_rkcb(book, &after_three_spades, Suit::Spades);
insert_uncontested(
book,
&[one_heart, one_spade, two_hearts],
responder_after_heart_rebid(),
);
insert_uncontested(
book,
&[one_heart, one_spade, two_hearts, three_hearts],
opener_accept_limit_raise(Suit::Hearts),
);
insert_uncontested(
book,
&[one_heart, one_spade, two_hearts, two_nt],
opener_after_heart_invite(),
);
for minor in [Suit::Clubs, Suit::Diamonds] {
let two_m = call(2, Strain::from(minor));
let three_m = call(3, Strain::from(minor));
insert_uncontested(
book,
&[one_heart, one_spade, two_m],
responder_after_minor_rebid(minor),
);
insert_uncontested(
book,
&[one_heart, one_spade, two_m, two_nt],
opener_accept_notrump_invite(),
);
insert_uncontested(
book,
&[one_heart, one_spade, two_m, three_m],
opener_accept_minor_raise(),
);
insert_uncontested(
book,
&[one_heart, one_spade, two_m, three_hearts],
opener_accept_limit_raise(Suit::Hearts),
);
}
if fourth_suit_forcing() {
let two_clubs = call(2, Strain::Clubs);
let two_diamonds = call(2, Strain::Diamonds);
let after_fourth_suit = [one_heart, one_spade, two_clubs, two_diamonds];
insert_uncontested(book, &after_fourth_suit, opener_after_fourth_suit());
let answers: Vec<Call> = {
let mut seen = std::collections::HashSet::new();
opener_after_fourth_suit()
.rules()
.iter()
.filter_map(|r| {
let c = r.call();
if seen.insert(c) { Some(c) } else { None }
})
.collect()
};
for answer in answers {
insert_uncontested(
book,
&[one_heart, one_spade, two_clubs, two_diamonds, answer],
responder_after_fourth_suit_answer(),
);
}
}
}
pub(super) fn register(book: &mut Trie) {
register_forcing_notrump_continuations(book);
register_major_jump_rebid_continuations(book);
insert_uncontested(
book,
&[call(1, Strain::Hearts), call(1, Strain::Spades)],
rebid_one_heart_one_spade(),
);
register_major_rebid_tails(book);
for major in [Suit::Hearts, Suit::Spades] {
insert_uncontested(
book,
&[call(1, Strain::from(major)), call(1, Strain::Notrump)],
rebid_after_forcing_notrump(major),
);
}
insert_uncontested(
book,
&[call(1, Strain::Clubs), call(1, Strain::Diamonds)],
rebid_one_club_one_diamond(),
);
for minor in [Suit::Clubs, Suit::Diamonds] {
for responder_major in [Suit::Hearts, Suit::Spades] {
insert_uncontested(
book,
&[
call(1, Strain::from(minor)),
call(1, Strain::from(responder_major)),
],
rebid_raise_major(responder_major, minor),
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bidding::System;
use contract_bridge::Hand;
use contract_bridge::auction::RelativeVulnerability;
fn tails_trie() -> Trie {
set_major_rebid_tails(true);
set_fourth_suit_forcing(false);
let mut trie = Trie::new();
register_major_rebid_tails(&mut trie);
set_fourth_suit_forcing(true);
trie
}
fn fsf_trie() -> Trie {
set_major_rebid_tails(true);
set_fourth_suit_forcing(true);
let mut trie = Trie::new();
register_major_rebid_tails(&mut trie);
trie
}
fn ladder_trie() -> Trie {
set_opener_extras_ladder(true);
let mut trie = Trie::new();
register(&mut trie);
trie
}
const AFTER_1D_1S: &[Call] = &[
Call::Bid(Bid::new(1, Strain::Diamonds)),
Call::Pass,
Call::Bid(Bid::new(1, Strain::Spades)),
Call::Pass,
];
#[test]
fn opener_extras_ladder_shows_strength() {
let trie = ladder_trie();
let b = |hand| best(&trie, AFTER_1D_1S, hand);
assert_eq!(
b("653.K3.AKQT854.A"),
Call::Bid(Bid::new(3, Strain::Diamonds))
);
assert_eq!(
b("T64.AJ86.AKQ95.A"),
Call::Bid(Bid::new(3, Strain::Hearts))
);
assert_eq!(b("K.62.AQJ94.AKJ85"), Call::Bid(Bid::new(3, Strain::Clubs)));
assert_eq!(
b("K54.Q3.KJ8542.32"),
Call::Bid(Bid::new(2, Strain::Diamonds))
);
}
#[test]
fn opener_extras_ladder_reverts_when_off() {
set_opener_extras_ladder(false);
let mut trie = Trie::new();
register(&mut trie);
set_opener_extras_ladder(true);
assert_eq!(
best(&trie, AFTER_1D_1S, "653.K3.AKQT854.A"),
Call::Bid(Bid::new(2, Strain::Diamonds))
);
}
fn major_jump_trie() -> Trie {
set_opener_major_jump_rebid(true);
let mut trie = Trie::new();
register(&mut trie);
trie
}
const AFTER_1H_1S: &[Call] = &[
Call::Bid(Bid::new(1, Strain::Hearts)),
Call::Pass,
Call::Bid(Bid::new(1, Strain::Spades)),
Call::Pass,
];
const AFTER_1S_1NT: &[Call] = &[
Call::Bid(Bid::new(1, Strain::Spades)),
Call::Pass,
Call::Bid(Bid::new(1, Strain::Notrump)),
Call::Pass,
];
#[test]
fn opener_major_jump_rebid_shows_strength() {
let trie = major_jump_trie();
assert_eq!(
best(&trie, AFTER_1H_1S, "3.AKQJ72.KQ5.J54"),
Call::Bid(Bid::new(3, Strain::Hearts))
);
assert_eq!(
best(&trie, AFTER_1H_1S, "A2.KQ9872.Q43.J5"),
Call::Bid(Bid::new(2, Strain::Hearts))
);
assert_eq!(
best(&trie, AFTER_1S_1NT, "AKQJ72.3.KQ5.J54"),
Call::Bid(Bid::new(3, Strain::Spades))
);
}
#[test]
fn opener_major_jump_rebid_reverts_when_off() {
set_opener_major_jump_rebid(false);
let mut trie = Trie::new();
register(&mut trie);
set_opener_major_jump_rebid(true);
assert_eq!(
best(&trie, AFTER_1H_1S, "3.AKQJ72.KQ5.J54"),
Call::Bid(Bid::new(2, Strain::Hearts))
);
}
const AFTER_1H_1S_3H: &[Call] = &[
Call::Bid(Bid::new(1, Strain::Hearts)),
Call::Pass,
Call::Bid(Bid::new(1, Strain::Spades)),
Call::Pass,
Call::Bid(Bid::new(3, Strain::Hearts)),
Call::Pass,
];
const AFTER_1S_1NT_3S: &[Call] = &[
Call::Bid(Bid::new(1, Strain::Spades)),
Call::Pass,
Call::Bid(Bid::new(1, Strain::Notrump)),
Call::Pass,
Call::Bid(Bid::new(3, Strain::Spades)),
Call::Pass,
];
#[test]
fn responder_accepts_major_jump_rebid() {
let trie = major_jump_trie();
assert_eq!(
best(&trie, AFTER_1H_1S_3H, "KQ85.K76.542.J43"),
Call::Bid(Bid::new(4, Strain::Hearts))
);
assert_eq!(
best(&trie, AFTER_1H_1S_3H, "KQ85.6.KJ43.Q642"),
Call::Bid(Bid::new(3, Strain::Notrump))
);
assert_eq!(best(&trie, AFTER_1H_1S_3H, "Q985.42.J8532.K4"), Call::Pass);
assert_eq!(
best(&trie, AFTER_1S_1NT_3S, "87.KQ86.KJ43.T92"),
Call::Bid(Bid::new(4, Strain::Spades))
);
}
fn best(trie: &Trie, auction: &[Call], hand: &str) -> Call {
let hand: Hand = hand.parse().expect("valid test hand");
let logits = trie
.classify(hand, RelativeVulnerability::NONE, auction)
.expect("trie covers this auction");
(&logits.0)
.into_iter()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).expect("logits are never NaN"))
.map(|(call, _)| call)
.expect("logits array is never empty")
}
const AFTER_2S: &[Call] = &[
Call::Bid(Bid::new(1, Strain::Hearts)),
Call::Pass,
Call::Bid(Bid::new(1, Strain::Spades)),
Call::Pass,
Call::Bid(Bid::new(2, Strain::Spades)),
Call::Pass,
];
const AFTER_2H: &[Call] = &[
Call::Bid(Bid::new(1, Strain::Hearts)),
Call::Pass,
Call::Bid(Bid::new(1, Strain::Spades)),
Call::Pass,
Call::Bid(Bid::new(2, Strain::Hearts)),
Call::Pass,
];
const AFTER_2H_2NT: &[Call] = &[
Call::Bid(Bid::new(1, Strain::Hearts)),
Call::Pass,
Call::Bid(Bid::new(1, Strain::Spades)),
Call::Pass,
Call::Bid(Bid::new(2, Strain::Hearts)),
Call::Pass,
Call::Bid(Bid::new(2, Strain::Notrump)),
Call::Pass,
];
const AFTER_2C: &[Call] = &[
Call::Bid(Bid::new(1, Strain::Hearts)),
Call::Pass,
Call::Bid(Bid::new(1, Strain::Spades)),
Call::Pass,
Call::Bid(Bid::new(2, Strain::Clubs)),
Call::Pass,
];
const AFTER_2C_2D: &[Call] = &[
Call::Bid(Bid::new(1, Strain::Hearts)),
Call::Pass,
Call::Bid(Bid::new(1, Strain::Spades)),
Call::Pass,
Call::Bid(Bid::new(2, Strain::Clubs)),
Call::Pass,
Call::Bid(Bid::new(2, Strain::Diamonds)),
Call::Pass,
];
const AFTER_2C_2D_2S: &[Call] = &[
Call::Bid(Bid::new(1, Strain::Hearts)),
Call::Pass,
Call::Bid(Bid::new(1, Strain::Spades)),
Call::Pass,
Call::Bid(Bid::new(2, Strain::Clubs)),
Call::Pass,
Call::Bid(Bid::new(2, Strain::Diamonds)),
Call::Pass,
Call::Bid(Bid::new(2, Strain::Spades)),
Call::Pass,
];
const AFTER_2C_2D_2NT: &[Call] = &[
Call::Bid(Bid::new(1, Strain::Hearts)),
Call::Pass,
Call::Bid(Bid::new(1, Strain::Spades)),
Call::Pass,
Call::Bid(Bid::new(2, Strain::Clubs)),
Call::Pass,
Call::Bid(Bid::new(2, Strain::Diamonds)),
Call::Pass,
Call::Bid(Bid::new(2, Strain::Notrump)),
Call::Pass,
];
#[test]
fn off_state_inserts_nothing() {
set_major_rebid_tails(false);
let mut trie = Trie::new();
register_major_rebid_tails(&mut trie);
set_major_rebid_tails(true); let hand: Hand = "K432.AQ5.432.Q32".parse().expect("valid test hand");
assert!(
trie.classify(hand, RelativeVulnerability::NONE, AFTER_2S)
.is_none(),
"the adjunct must insert zero nodes while off"
);
}
#[test]
fn spade_raise_responder_picks_by_points() {
let trie = tails_trie();
assert_eq!(
best(&trie, AFTER_2S, "A432.KQ5.K54.J32"),
Call::Bid(Bid::new(4, Strain::Spades)),
"13 points -> 4♠"
);
assert_eq!(
best(&trie, AFTER_2S, "A432.K54.Q54.J32"),
Call::Bid(Bid::new(3, Strain::Spades)),
"10 points -> 3♠"
);
assert_eq!(
best(&trie, AFTER_2S, "A432.432.Q54.J32"),
Call::Pass,
"7 points -> pass"
);
}
#[test]
fn heart_rebid_responder_prefers_the_fit() {
let trie = tails_trie();
assert_eq!(
best(&trie, AFTER_2H, "K987.AQ.9876.Q32"),
Call::Bid(Bid::new(3, Strain::Hearts)),
"11 points, 2 hearts -> 3♥ beats 2NT"
);
assert_eq!(
best(&trie, AFTER_2H, "AJ98.K.7654.QJ87"),
Call::Bid(Bid::new(2, Strain::Notrump)),
"11 points, 1 heart -> 2NT"
);
}
#[test]
fn opener_answers_the_heart_invite_by_hcp() {
let trie = tails_trie();
assert_eq!(
best(&trie, AFTER_2H_2NT, "AK32.KQ32.A32.32"),
Call::Bid(Bid::new(3, Strain::Notrump)),
"16 HCP -> 3NT"
);
assert_eq!(
best(&trie, AFTER_2H_2NT, "K432.KQ32.Q32.Q3"),
Call::Bid(Bid::new(3, Strain::Hearts)),
"12 HCP -> 3♥ retreat"
);
}
#[test]
fn minor_rebid_responder_picks_by_weight() {
let trie = tails_trie();
assert_eq!(
best(&trie, AFTER_2C, "K432.AQ5.432.Q32"),
Call::Bid(Bid::new(3, Strain::Hearts)),
"11 points, 3 hearts -> 3♥"
);
assert_eq!(
best(&trie, AFTER_2C, "K432.98.76.AQJ54"),
Call::Bid(Bid::new(3, Strain::Clubs)),
"11 points, 5 clubs, no 3-heart holding -> 3♣"
);
assert_eq!(
best(&trie, AFTER_2C, "K432.Q3.K432.432"),
Call::Bid(Bid::new(2, Strain::Hearts)),
"8 points, 2 hearts -> 2♥"
);
assert_eq!(
best(&trie, AFTER_2C, "K98765.4.Q32.J32"),
Call::Bid(Bid::new(2, Strain::Spades)),
"weak hand, 6 spades -> 2♠"
);
assert_eq!(
best(&trie, AFTER_2C, "AK32.Q54.K54.Q32"),
Call::Bid(Bid::new(3, Strain::Notrump)),
"14 points, no fit -> 3NT"
);
}
#[test]
fn fourth_suit_forcing_fires_at_twelve_points() {
let trie = fsf_trie();
assert_eq!(
best(&trie, AFTER_2C, "AK32.Q54.K54.Q32"),
Call::Bid(Bid::new(2, Strain::Diamonds)),
"14 points -> 2♦ fourth-suit-forcing"
);
assert_eq!(
best(&trie, AFTER_2C, "K432.AQ5.432.Q32"),
Call::Bid(Bid::new(3, Strain::Hearts)),
"11 points, 3 hearts -> 3♥ (below the FSF floor)"
);
}
#[test]
fn fourth_suit_forcing_opener_answers_by_weight() {
let trie = fsf_trie();
assert_eq!(
best(&trie, AFTER_2C_2D, "KQ4.AJ76.A32.987"),
Call::Bid(Bid::new(2, Strain::Spades)),
"3 spades + diamond stopper -> 2♠ beats 2NT"
);
assert_eq!(
best(&trie, AFTER_2C_2D, "98.K8765.432.876"),
Call::Bid(Bid::new(2, Strain::Hearts)),
"none of the above -> 2♥ catch-all"
);
}
#[test]
fn fourth_suit_forcing_responder_places_the_contract() {
let trie = fsf_trie();
assert_eq!(
best(&trie, AFTER_2C_2D_2S, "AKJ87.654.32.432"),
Call::Bid(Bid::new(4, Strain::Spades)),
"5 spades after 2♠ -> 4♠"
);
assert_eq!(
best(&trie, AFTER_2C_2D_2NT, "Q432.KJ8.654.J32"),
Call::Bid(Bid::new(4, Strain::Hearts)),
"3 hearts after 2NT -> 4♥"
);
assert_eq!(
best(&trie, AFTER_2C_2D_2NT, "Q432.K8.6543.J32"),
Call::Bid(Bid::new(3, Strain::Notrump)),
"neither fit -> 3NT"
);
}
#[test]
fn fourth_suit_forcing_without_tails_inserts_nothing() {
set_major_rebid_tails(false);
set_fourth_suit_forcing(true);
let mut trie = Trie::new();
register_major_rebid_tails(&mut trie);
set_major_rebid_tails(true);
let hand: Hand = "K432.AQ5.432.Q32".parse().expect("valid test hand");
assert!(
trie.classify(hand, RelativeVulnerability::NONE, AFTER_2C)
.is_none(),
"fourth-suit-forcing must not register without the tails adjunct"
);
}
#[test]
fn fourth_suit_forcing_rule_is_alerted() {
set_fourth_suit_forcing(true);
let rules = responder_after_minor_rebid(Suit::Clubs);
let fsf_rule = rules
.rules()
.iter()
.find(|r| r.call() == Call::Bid(Bid::new(2, Strain::Diamonds)))
.expect("the fourth-suit-forcing rule is present");
assert!(
fsf_rule.alert().is_some(),
"fourth-suit-forcing must carry an alert"
);
}
#[test]
fn balanced_1nt_rebid_knob_flips_2m_to_1nt() {
let one_d_one_h = &[
call(1, Strain::Diamonds),
Call::Pass,
call(1, Strain::Hearts),
Call::Pass,
];
let hand = "KQ4.Q3.AK762.853";
let build = || {
let mut trie = Trie::new();
insert_uncontested(
&mut trie,
&[call(1, Strain::Diamonds), call(1, Strain::Hearts)],
rebid_raise_major(Suit::Hearts, Suit::Diamonds),
);
trie
};
set_balanced_1nt_rebid(false);
assert_eq!(best(&build(), one_d_one_h, hand), call(2, Strain::Diamonds));
set_balanced_1nt_rebid(true); let on = build();
assert_eq!(best(&on, one_d_one_h, hand), call(1, Strain::Notrump));
}
}