use super::{call, insert_uncontested, slam};
use crate::bidding::constraint::{balanced, hcp, len};
use crate::bidding::{Rules, Trie};
use contract_bridge::auction::Call;
use contract_bridge::{Strain, Suit};
fn side_suits(major: Suit) -> [Suit; 3] {
match major {
Suit::Hearts => [Suit::Clubs, Suit::Diamonds, Suit::Spades],
Suit::Spades => [Suit::Clubs, Suit::Diamonds, Suit::Hearts],
_ => unreachable!("Stenberg 2NT only applies to a major-suit opening"),
}
}
fn stenberg_rebids(major: Suit) -> Rules {
let [a, b, c] = side_suits(major);
let rules = Rules::new()
.rule(call(3, Strain::Clubs), 0.5, hcp(..15))
.rule(
call(3, Strain::Diamonds),
1.5,
hcp(15..) & !len(a, ..=1) & !len(b, ..=1) & !len(c, ..=1),
)
.rule(call(3, Strain::Hearts), 2.00, hcp(15..) & len(a, ..=1))
.rule(call(3, Strain::Spades), 1.98, hcp(15..) & len(b, ..=1))
.rule(call(3, Strain::Notrump), 1.96, hcp(15..) & len(c, ..=1))
.rule(
call(4, Strain::Clubs),
2.20,
hcp(15..) & len(Suit::Clubs, 5..),
)
.rule(
call(4, Strain::Diamonds),
2.15,
hcp(15..) & len(Suit::Diamonds, 5..),
);
if major == Suit::Hearts {
rules.rule(
call(4, Strain::Hearts),
2.30,
hcp(15..) & len(Suit::Hearts, 6..) & len(Suit::Spades, 4..),
)
} else {
rules
.rule(
call(4, Strain::Hearts),
2.20,
hcp(15..) & len(Suit::Hearts, 5..),
)
.rule(
call(4, Strain::Spades),
1.0,
hcp(..15) & len(Suit::Hearts, 5..),
)
}
}
fn responder_after_min(major: Suit) -> Rules {
let t = Strain::from(major);
Rules::new()
.rule(call(4, Strain::Notrump), 1.0, hcp(15..))
.rule(call(3, Strain::Notrump), 0.8, hcp(13..=15) & balanced())
.rule(call(4, t), 0.5, hcp(0..))
}
fn responder_after_descriptive(major: Suit, rebid: Call) -> Rules {
let t = Strain::from(major);
let game = call(4, t);
let rules = Rules::new().rule(call(4, Strain::Notrump), 1.0, hcp(15..));
if rebid == game {
rules.rule(Call::Pass, 0.0, hcp(0..))
} else {
rules.rule(game, 0.5, hcp(0..))
}
}
pub(super) fn register(book: &mut Trie) {
for major in [Suit::Hearts, Suit::Spades] {
let t = Strain::from(major);
let our = [call(1, t), call(2, Strain::Notrump)];
let rebids = stenberg_rebids(major);
let distinct: Vec<Call> = {
let mut seen = std::collections::HashSet::new();
rebids
.rules()
.iter()
.filter_map(|r| seen.insert(r.call()).then_some(r.call()))
.collect()
};
insert_uncontested(book, &our, rebids);
for rebid in distinct {
let resp_calls = [call(1, t), call(2, Strain::Notrump), rebid];
let resp = if rebid == call(3, Strain::Clubs) {
responder_after_min(major)
} else {
responder_after_descriptive(major, rebid)
};
insert_uncontested(book, &resp_calls, resp);
slam::install_rkcb(book, &resp_calls, major);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bidding::Rules;
use crate::bidding::context::Context;
use crate::bidding::trie::Classifier;
use contract_bridge::Hand;
use contract_bridge::auction::RelativeVulnerability;
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")
}
const RAISE: [Call; 2] = [call(1, Strain::Hearts), call(2, Strain::Notrump)];
#[test]
fn minimum_opener_bids_three_clubs() {
let r = stenberg_rebids(Suit::Hearts);
assert_eq!(best(&r, &RAISE, "A8.KQ954.K84.J92"), call(3, Strain::Clubs));
}
#[test]
fn maximum_no_shortness_bids_three_diamonds() {
let r = stenberg_rebids(Suit::Hearts);
assert_eq!(
best(&r, &RAISE, "A8.AQJ54.KQ4.Q92"),
call(3, Strain::Diamonds)
);
}
#[test]
fn maximum_club_fragment_bids_three_hearts() {
let r = stenberg_rebids(Suit::Hearts);
assert_eq!(
best(&r, &RAISE, "AQ4.KQ954.KJ84.2"),
call(3, Strain::Hearts)
);
}
#[test]
fn maximum_two_suiter_bids_four_hearts_over_one_heart() {
let r = stenberg_rebids(Suit::Hearts);
assert_eq!(
best(&r, &RAISE, "AQ84.AK9542.Q2.2"),
call(4, Strain::Hearts)
);
}
#[test]
fn spade_minimum_with_five_hearts_bids_four_spades() {
let r = stenberg_rebids(Suit::Spades);
let raise = [call(1, Strain::Spades), call(2, Strain::Notrump)];
assert_eq!(
best(&r, &raise, "KQ954.AJ842.K4.2"),
call(4, Strain::Spades)
);
}
}