use super::insert_uncontested;
use crate::bidding::constraint::{
Cons, Constraint, balanced, described, fifths, hcp, len, nth_seat, points, rule_of_20,
};
use crate::bidding::context::Context;
use crate::bidding::{Alert, Rules, Trie};
use contract_bridge::auction::Call;
use contract_bridge::{Bid, Hand, Strain, Suit};
use std::cell::Cell;
const STRONG_2C: Alert = Alert("strong-2c");
thread_local! {
static OPEN_ONE_NOTRUMP: Cell<bool> = const { Cell::new(true) };
static ONE_NOTRUMP_FIFTHS: Cell<bool> = const { Cell::new(false) };
static RULE_OF_20: Cell<bool> = const { Cell::new(true) };
}
pub fn set_open_one_notrump(on: bool) {
OPEN_ONE_NOTRUMP.with(|cell| cell.set(on));
}
pub fn set_one_notrump_fifths(on: bool) {
ONE_NOTRUMP_FIFTHS.with(|cell| cell.set(on));
}
pub fn set_rule_of_20(on: bool) {
RULE_OF_20.with(|cell| cell.set(on));
}
pub(crate) fn rule_of_20_enabled() -> bool {
RULE_OF_20.with(Cell::get)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NotrumpShape {
Balanced,
Wide,
Wide6322,
}
fn notrump_shape(shape: NotrumpShape) -> Cons<impl Constraint + Clone> {
balanced()
| described("wide 1NT shape", move |hand: Hand, _: &Context<'_>| {
let mut lengths = Suit::ASC.map(|suit| hand[suit].len());
lengths.sort_unstable();
let long = match (shape, lengths) {
(NotrumpShape::Balanced, _) => return false,
(_, [2, 2, 4, 5]) => 5,
(NotrumpShape::Wide6322, [2, 2, 3, 6]) => 6,
_ => return false,
};
hand[Suit::Clubs].len() == long || hand[Suit::Diamonds].len() == long
})
}
fn prefers_diamonds() -> Cons<impl Constraint + Clone> {
described("prefers diamonds", |hand: Hand, _: &Context<'_>| {
let clubs = hand[Suit::Clubs].len();
let diamonds = hand[Suit::Diamonds].len();
diamonds > clubs || (diamonds == clubs && diamonds >= 4)
})
}
#[must_use]
pub fn openings() -> Rules {
openings_with(NotrumpShape::Wide)
}
#[must_use]
pub fn openings_with(shape: NotrumpShape) -> Rules {
let mut rules = Rules::new()
.rule(Bid::new(2, Strain::Clubs), 3.0, points(22..))
.alert(STRONG_2C);
if OPEN_ONE_NOTRUMP.with(Cell::get) {
rules = if ONE_NOTRUMP_FIFTHS.with(Cell::get) {
rules.rule(
Bid::new(1, Strain::Notrump),
2.0,
fifths(14.5..17.5) & notrump_shape(shape),
)
} else {
rules.rule(
Bid::new(1, Strain::Notrump),
2.0,
hcp(15..=17) & notrump_shape(shape),
)
};
}
rules = rules
.rule(
Bid::new(2, Strain::Notrump),
2.0,
fifths(20.0..22.0) & balanced(),
)
.rule(
Bid::new(1, Strain::Spades),
1.6,
points(12..=21) & len(Suit::Spades, 5..),
)
.rule(
Bid::new(1, Strain::Hearts),
1.5,
points(12..=21) & len(Suit::Hearts, 5..),
)
.rule(
Bid::new(1, Strain::Spades),
2.6,
points(9..=11) & len(Suit::Spades, 5..) & (nth_seat(3) | nth_seat(4)),
)
.rule(
Bid::new(1, Strain::Hearts),
2.5,
points(9..=11) & len(Suit::Hearts, 5..) & (nth_seat(3) | nth_seat(4)),
)
.rule(
Bid::new(1, Strain::Diamonds),
1.0,
points(12..=21) & prefers_diamonds() & len(Suit::Hearts, ..5) & len(Suit::Spades, ..5),
)
.rule(
Bid::new(1, Strain::Clubs),
1.0,
points(12..=21)
& len(Suit::Clubs, 3..)
& !prefers_diamonds()
& len(Suit::Hearts, ..5)
& len(Suit::Spades, ..5),
);
for suit in [Suit::Diamonds, Suit::Hearts, Suit::Spades] {
rules = rules.rule(
Bid::new(2, Strain::from(suit)),
1.0,
len(suit, 6..=6) & points(5..=10) & !nth_seat(4),
);
}
for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
rules = rules.rule(
Bid::new(3, Strain::from(suit)),
0.9,
len(suit, 7..) & points(..12) & !nth_seat(4),
);
}
if RULE_OF_20.with(Cell::get) {
rules = rules
.rule(
Bid::new(1, Strain::Spades),
1.6,
hcp(10..=11) & rule_of_20() & len(Suit::Spades, 5..),
)
.rule(
Bid::new(1, Strain::Hearts),
1.5,
hcp(10..=11) & rule_of_20() & len(Suit::Hearts, 5..),
)
.rule(
Bid::new(1, Strain::Diamonds),
1.0,
hcp(10..=11)
& rule_of_20()
& prefers_diamonds()
& len(Suit::Hearts, ..5)
& len(Suit::Spades, ..5),
)
.rule(
Bid::new(1, Strain::Clubs),
1.0,
hcp(10..=11)
& rule_of_20()
& len(Suit::Clubs, 3..)
& !prefers_diamonds()
& len(Suit::Hearts, ..5)
& len(Suit::Spades, ..5),
);
}
rules.rule(Call::Pass, 0.0, points(..12))
}
pub(super) fn register(book: &mut Trie, shape: NotrumpShape) {
insert_uncontested(book, &[], openings_with(shape));
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bidding::context::Context;
use crate::bidding::trie::Classifier;
use contract_bridge::auction::RelativeVulnerability;
fn opens(rules: &Rules, hand: &str) -> Call {
let hand: Hand = hand.parse().expect("valid test hand");
let context = Context::new(RelativeVulnerability::NONE, &[]);
let logits = rules.classify(hand, &context);
(&logits.0)
.into_iter()
.max_by(|(_, a): &(Call, &f32), (_, b)| a.partial_cmp(b).expect("logits are never NaN"))
.map(|(call, _)| call)
.expect("array is never empty")
}
#[test]
fn wide_notrump_shape_gate() {
let one_nt = Call::Bid(Bid::new(1, Strain::Notrump));
let one_s = Call::Bid(Bid::new(1, Strain::Spades));
let one_c = Call::Bid(Bid::new(1, Strain::Clubs));
let five422_minor = "Q432.KQ.K2.AK432";
let five422_major = "AK432.KQ.Q432.K2";
let six322_minor = "Q2.K3.AQ4.KQ8765";
let six322_major = "KQ8765.K3.AQ4.Q2";
let balanced16 = "AQ32.K53.QJ4.A92";
let narrow = openings_with(NotrumpShape::Balanced);
assert_eq!(opens(&narrow, balanced16), one_nt);
assert_eq!(opens(&narrow, five422_minor), one_c);
assert_eq!(opens(&narrow, five422_major), one_s);
assert_eq!(opens(&narrow, six322_minor), one_c);
assert_eq!(opens(&narrow, six322_major), one_s);
let wide = openings_with(NotrumpShape::Wide);
assert_eq!(opens(&wide, balanced16), one_nt);
assert_eq!(opens(&wide, five422_minor), one_nt);
assert_eq!(opens(&wide, five422_major), one_s);
assert_eq!(opens(&wide, six322_minor), one_c);
assert_eq!(opens(&wide, six322_major), one_s);
let wide6322 = openings_with(NotrumpShape::Wide6322);
assert_eq!(opens(&wide6322, five422_minor), one_nt);
assert_eq!(opens(&wide6322, five422_major), one_s);
assert_eq!(opens(&wide6322, six322_minor), one_nt);
assert_eq!(opens(&wide6322, six322_major), one_s);
}
#[test]
fn suppress_one_notrump_opens_a_minor() {
let one_nt = Call::Bid(Bid::new(1, Strain::Notrump));
let one_c = Call::Bid(Bid::new(1, Strain::Clubs));
let balanced16 = "AQ32.K53.QJ4.A92";
assert_eq!(opens(&openings(), balanced16), one_nt);
set_open_one_notrump(false);
let call = opens(&openings(), balanced16);
set_open_one_notrump(true);
assert_eq!(call, one_c);
}
#[test]
fn rule_of_20_opens_sound_eleven_counts() {
let one_s = Call::Bid(Bid::new(1, Strain::Spades));
let sound_11 = "AK986.J9.QJT6.64";
assert_eq!(opens(&openings(), sound_11), one_s);
set_rule_of_20(false);
let call = opens(&openings(), sound_11);
set_rule_of_20(true);
assert_eq!(call, Call::Pass);
}
}