use super::{call, insert_uncontested, slam};
use crate::bidding::constraint::{fifths, hcp, len, points, top_honors};
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 MAJOR_GAME_TRIES: Cell<bool> = const { Cell::new(true) };
static LIMIT_RAISE_ACCEPTANCE: Cell<bool> = const { Cell::new(true) };
}
pub fn set_major_game_tries(on: bool) {
MAJOR_GAME_TRIES.with(|cell| cell.set(on));
}
fn major_game_tries() -> bool {
MAJOR_GAME_TRIES.with(Cell::get)
}
pub fn set_limit_raise_acceptance(on: bool) {
LIMIT_RAISE_ACCEPTANCE.with(|cell| cell.set(on));
}
fn limit_raise_acceptance() -> bool {
LIMIT_RAISE_ACCEPTANCE.with(Cell::get)
}
const SHORTNESS: Alert = Alert("shortness");
fn jacoby_rebids(major: Suit) -> Rules {
let trump = Strain::from(major);
let side_suits: Vec<Suit> = [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades]
.into_iter()
.filter(|&s| s != major)
.collect();
let mut rules = Rules::new();
for &side in &side_suits {
if Strain::from(side) < trump {
rules = rules.rule(
Bid::new(4, Strain::from(side)),
2.2,
len(side, 5..) & top_honors(side, 2..),
);
}
}
for &side in &side_suits {
rules = rules
.rule(Bid::new(3, Strain::from(side)), 2.0, len(side, ..=1))
.alert(SHORTNESS);
}
let [a, b, c] = [side_suits[0], side_suits[1], side_suits[2]];
let no_shortness = !len(a, ..=1) & !len(b, ..=1) & !len(c, ..=1);
rules = rules.rule(Bid::new(3, trump), 1.5, points(18..) & no_shortness.clone());
rules = rules.rule(
Bid::new(3, Strain::Notrump),
1.4,
fifths(15.0..18.0) & no_shortness,
);
rules.rule(Bid::new(4, trump), 0.5, hcp(0..))
}
fn responder_after_jacoby(major: Suit, opener_bid: Call) -> Rules {
let four_major = call(4, Strain::from(major));
let four_nt = call(4, Strain::Notrump);
if opener_bid == four_major {
Rules::new()
.rule(four_nt, 1.0, points(18..))
.alert(slam::RKCB)
.rule(Call::Pass, 0.0, hcp(0..))
} else {
Rules::new()
.rule(four_nt, 1.0, points(16..))
.alert(slam::RKCB)
.rule(four_major, 0.5, hcp(0..))
}
}
fn try_level(major: Suit, suit: Suit) -> u8 {
if Strain::from(suit) > Strain::from(major) {
2
} else {
3
}
}
fn game_try_suits(major: Suit) -> Vec<Suit> {
let major_strain = Strain::from(major);
let mut above = Vec::new();
let mut below = Vec::new();
for suit in [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades] {
if suit == major {
continue;
}
if Strain::from(suit) > major_strain {
above.push(suit);
} else {
below.push(suit);
}
}
above.into_iter().chain(below).collect()
}
#[must_use]
fn opener_after_raise(major: Suit) -> Rules {
let trump = Strain::from(major);
let mut rules = Rules::new()
.rule(Bid::new(4, Strain::Notrump), 2.6, points(22..))
.alert(slam::RKCB)
.rule(Bid::new(4, trump), 2.2, points(19..));
for (suit, weight) in game_try_suits(major).into_iter().zip([1.5_f32, 1.45, 1.40]) {
rules = rules.rule(
Bid::new(try_level(major, suit), Strain::from(suit)),
weight,
len(suit, 4..) & points(16..=18),
);
}
rules
.rule(Bid::new(3, trump), 1.2, points(16..=18))
.rule(Call::Pass, 0.0, hcp(0..))
}
#[must_use]
fn responder_after_try(major: Suit, try_suit: Suit) -> Rules {
let trump = Strain::from(major);
Rules::new()
.rule(
Bid::new(4, trump),
1.0,
points(8..=9) | len(try_suit, ..=1) | top_honors(try_suit, 2..),
)
.rule(Bid::new(3, trump), 0.5, hcp(0..))
}
#[must_use]
fn responder_after_general_try(major: Suit) -> Rules {
Rules::new()
.rule(Bid::new(4, Strain::from(major)), 1.0, points(8..=9))
.rule(Call::Pass, 0.0, hcp(0..))
}
#[must_use]
fn opener_after_decline(major: Suit) -> Rules {
Rules::new()
.rule(Bid::new(4, Strain::from(major)), 1.0, points(18..))
.rule(Call::Pass, 0.0, hcp(0..))
}
fn register_major_game_tries(book: &mut Trie) {
if !major_game_tries() {
return;
}
for major in [Suit::Hearts, Suit::Spades] {
let trump = Strain::from(major);
let raise_calls = [call(1, trump), call(2, trump)];
insert_uncontested(book, &raise_calls, opener_after_raise(major));
slam::install_rkcb(book, &raise_calls, major);
for suit in game_try_suits(major) {
let try_call = call(try_level(major, suit), Strain::from(suit));
let try_calls = [raise_calls[0], raise_calls[1], try_call];
insert_uncontested(book, &try_calls, responder_after_try(major, suit));
let decline_calls = [try_calls[0], try_calls[1], try_calls[2], call(3, trump)];
insert_uncontested(book, &decline_calls, opener_after_decline(major));
}
let general_try_calls = [raise_calls[0], raise_calls[1], call(3, trump)];
insert_uncontested(book, &general_try_calls, responder_after_general_try(major));
}
}
#[must_use]
fn opener_after_limit_raise(major: Suit) -> Rules {
let trump = Strain::from(major);
Rules::new()
.rule(Bid::new(4, Strain::Notrump), 1.5, points(19..))
.alert(slam::RKCB)
.rule(Bid::new(4, trump), 1.0, points(13..))
.rule(Call::Pass, 0.0, hcp(0..))
}
fn register_limit_raise_acceptance(book: &mut Trie) {
if !limit_raise_acceptance() {
return;
}
for major in [Suit::Hearts, Suit::Spades] {
let limit_calls = [call(1, Strain::from(major)), call(3, Strain::from(major))];
insert_uncontested(book, &limit_calls, opener_after_limit_raise(major));
slam::install_rkcb(book, &limit_calls, major);
}
}
pub(super) fn register(book: &mut Trie) {
for major in [Suit::Hearts, Suit::Spades] {
let our_calls = [call(1, Strain::from(major)), call(2, Strain::Notrump)];
let rebids = jacoby_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_calls, rebids);
for opener_bid in distinct {
let resp = responder_after_jacoby(major, opener_bid);
let resp_calls: [Call; 3] = [
call(1, Strain::from(major)),
call(2, Strain::Notrump),
opener_bid,
];
insert_uncontested(book, &resp_calls, resp);
slam::install_rkcb(book, &resp_calls, major);
}
}
register_major_game_tries(book);
register_limit_raise_acceptance(book);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bidding::System;
use contract_bridge::Hand;
use contract_bridge::auction::RelativeVulnerability;
fn hand(s: &str) -> Hand {
s.parse().expect("valid test hand")
}
fn best(trie: &Trie, auction: &[Call], hand_str: &str) -> Call {
let logits = trie
.classify(hand(hand_str), 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")
}
fn game_tries_trie() -> Trie {
set_major_game_tries(true);
let mut trie = Trie::new();
register(&mut trie);
trie
}
fn limit_raise_trie() -> Trie {
set_limit_raise_acceptance(true);
let mut trie = Trie::new();
register(&mut trie);
trie
}
const RAISE_AUCTION: &[Call] = &[
Call::Bid(Bid::new(1, Strain::Hearts)),
Call::Pass,
Call::Bid(Bid::new(2, Strain::Hearts)),
Call::Pass,
];
const LIMIT_RAISE_AUCTION: &[Call] = &[
Call::Bid(Bid::new(1, Strain::Hearts)),
Call::Pass,
Call::Bid(Bid::new(3, Strain::Hearts)),
Call::Pass,
];
#[test]
fn game_tries_absent_when_off() {
set_major_game_tries(false);
set_limit_raise_acceptance(false);
let mut trie = Trie::new();
register(&mut trie);
set_major_game_tries(true); set_limit_raise_acceptance(true);
assert!(
trie.classify(
hand("K52.AK974.3.AQ65"),
RelativeVulnerability::NONE,
RAISE_AUCTION
)
.is_none(),
"major game tries must be absent with the knob off"
);
}
#[test]
fn limit_raise_acceptance_absent_when_off() {
set_major_game_tries(false);
set_limit_raise_acceptance(false);
let mut trie = Trie::new();
register(&mut trie);
set_major_game_tries(true); set_limit_raise_acceptance(true);
assert!(
trie.classify(
hand("A63.AK975.QJ3.83"),
RelativeVulnerability::NONE,
LIMIT_RAISE_AUCTION
)
.is_none(),
"limit-raise acceptance must be absent with the knob off"
);
}
#[test]
fn opener_tries_the_long_club_suit() {
let trie = game_tries_trie();
assert_eq!(
best(&trie, RAISE_AUCTION, "K52.AK974.3.AQ65"),
Call::Bid(Bid::new(3, Strain::Clubs)),
);
}
#[test]
fn opener_bids_game_with_a_maximum() {
let trie = game_tries_trie();
assert_eq!(
best(&trie, RAISE_AUCTION, "AQ3.AKQ85.KJ4.92"),
Call::Bid(Bid::new(4, Strain::Hearts)),
);
}
#[test]
fn opener_passes_a_flat_minimum() {
let trie = game_tries_trie();
assert_eq!(best(&trie, RAISE_AUCTION, "KQ3.AJ854.K63.93"), Call::Pass);
}
fn after_club_try() -> Vec<Call> {
RAISE_AUCTION
.iter()
.copied()
.chain([Call::Bid(Bid::new(3, Strain::Clubs)), Call::Pass])
.collect()
}
#[test]
fn responder_accepts_the_try_with_a_singleton() {
let trie = game_tries_trie();
assert_eq!(
best(&trie, &after_club_try(), "863.K64.QJ8532.7"),
Call::Bid(Bid::new(4, Strain::Hearts)),
);
}
#[test]
fn responder_declines_a_wasted_minimum() {
let trie = game_tries_trie();
assert_eq!(
best(&trie, &after_club_try(), "863.K64.QJ85.972"),
Call::Bid(Bid::new(3, Strain::Hearts)),
);
}
#[test]
fn limit_raise_accepts_and_declines() {
let trie = limit_raise_trie();
assert_eq!(
best(&trie, LIMIT_RAISE_AUCTION, "A63.AK975.Q43.83"),
Call::Bid(Bid::new(4, Strain::Hearts)),
);
assert_eq!(
best(&trie, LIMIT_RAISE_AUCTION, "AJ6.K8532.Q63.Q7"),
Call::Pass
);
}
}