use crate::bidding::{Rules, Trie};
use contract_bridge::auction::Call;
use contract_bridge::{Bid, Rank, Strain, Suit};
use core::ops::RangeBounds;
use std::sync::Arc;
use super::{insert_uncontested, uncontested};
use crate::bidding::constraint::{hcp, pred};
use crate::bidding::trie::Classifier;
use contract_bridge::Hand;
fn insert_arc_all_seats(
book: &mut Trie,
suffix: &[Call],
max_passes: usize,
f: Arc<dyn Classifier>,
) {
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(&f));
}
}
fn count_keycards(hand: Hand, trump: Suit) -> usize {
let aces = Suit::ASC
.into_iter()
.filter(|&s| hand[s].contains(Rank::A))
.count();
let trump_king = usize::from(hand[trump].contains(Rank::K));
aces + trump_king
}
fn count_kings_outside(hand: Hand, trump: Suit) -> usize {
Suit::ASC
.into_iter()
.filter(|&s| s != trump && hand[s].contains(Rank::K))
.count()
}
fn keycards(
trump: Suit,
range: impl RangeBounds<usize> + Clone + Send + Sync + 'static,
) -> crate::bidding::constraint::Cons<impl crate::bidding::constraint::Constraint + Clone> {
pred(
move |hand: Hand, _: &crate::bidding::context::Context<'_>| {
range.contains(&count_keycards(hand, trump))
},
)
}
fn has_trump_queen(
trump: Suit,
) -> crate::bidding::constraint::Cons<impl crate::bidding::constraint::Constraint + Clone> {
pred(move |hand: Hand, _: &crate::bidding::context::Context<'_>| hand[trump].contains(Rank::Q))
}
fn kings_outside(
trump: Suit,
range: impl RangeBounds<usize> + Clone + Send + Sync + 'static,
) -> crate::bidding::constraint::Cons<impl crate::bidding::constraint::Constraint + Clone> {
pred(
move |hand: Hand, _: &crate::bidding::context::Context<'_>| {
range.contains(&count_kings_outside(hand, trump))
},
)
}
fn rkcb_answers(trump: Suit) -> Rules {
Rules::new()
.rule(
Bid::new(5, Strain::Clubs),
1.0,
keycards(trump, 1..=1) | keycards(trump, 4..=4),
)
.rule(
Bid::new(5, Strain::Diamonds),
1.0,
keycards(trump, 0..=0) | keycards(trump, 3..=3),
)
.rule(
Bid::new(5, Strain::Hearts),
1.0,
keycards(trump, 2..=2) & !has_trump_queen(trump),
)
.rule(
Bid::new(5, Strain::Spades),
1.0,
keycards(trump, 2..=2) & has_trump_queen(trump),
)
}
fn asker_after_5c(trump: Suit) -> Rules {
let t = Strain::from(trump);
Rules::new()
.rule(Bid::new(5, Strain::Notrump), 1.4, keycards(trump, 4..=4))
.rule(Bid::new(6, t), 1.0, keycards(trump, 3..=3))
.rule(Bid::new(5, t), 0.5, hcp(0..))
}
fn asker_after_5d(trump: Suit) -> Rules {
let t = Strain::from(trump);
Rules::new()
.rule(
Bid::new(6, t),
1.0,
keycards(trump, 2..=2) | keycards(trump, 4..),
)
.rule(Bid::new(5, t), 0.5, hcp(0..))
}
fn asker_after_5h(trump: Suit) -> Rules {
let t = Strain::from(trump);
Rules::new()
.rule(Bid::new(6, t), 1.0, keycards(trump, 3..))
.rule(Bid::new(5, t), 0.5, hcp(0..))
}
fn asker_after_5s(trump: Suit) -> Rules {
let t = Strain::from(trump);
let mut rules = Rules::new()
.rule(
Bid::new(5, Strain::Notrump),
1.4,
keycards(trump, 3..) & kings_outside(trump, 2..),
)
.rule(Bid::new(6, t), 1.0, keycards(trump, 2..))
.rule(Bid::new(5, t), 0.5, hcp(0..));
if trump == Suit::Hearts {
rules = rules.rule(Bid::new(6, t), 0.3, hcp(0..));
}
rules
}
fn king_answers(trump: Suit) -> Rules {
let mut rules = Rules::new()
.rule(Bid::new(6, Strain::Clubs), 1.0, kings_outside(trump, 0..=0))
.rule(
Bid::new(6, Strain::Diamonds),
1.0,
kings_outside(trump, 1..=1),
);
match trump {
Suit::Spades => {
rules = rules
.rule(
Bid::new(6, Strain::Hearts),
1.0,
kings_outside(trump, 2..=2),
)
.rule(Bid::new(6, Strain::Spades), 0.5, hcp(0..));
}
Suit::Hearts => {
rules = rules.rule(Bid::new(6, Strain::Hearts), 0.5, hcp(0..));
}
_ => unreachable!("install_rkcb only supports major-suit trumps"),
}
rules
}
fn asker_after_6c(trump: Suit) -> Rules {
let t = Strain::from(trump);
Rules::new()
.rule(Bid::new(7, t), 1.0, kings_outside(trump, 3..))
.rule(Bid::new(6, t), 0.5, hcp(0..))
}
fn asker_after_6d(trump: Suit) -> Rules {
let t = Strain::from(trump);
Rules::new()
.rule(Bid::new(7, t), 1.0, kings_outside(trump, 2..))
.rule(Bid::new(6, t), 0.5, hcp(0..))
}
fn asker_after_6h(trump: Suit) -> Rules {
let t = Strain::from(trump);
Rules::new()
.rule(Bid::new(7, t), 1.0, kings_outside(trump, 1..))
.rule(Bid::new(6, t), 0.5, hcp(0..))
}
pub(super) fn install_rkcb(book: &mut Trie, our_calls: &[Call], trump: Suit) {
let c_4nt = Call::Bid(Bid::new(4, Strain::Notrump));
let ans_5c = Call::Bid(Bid::new(5, Strain::Clubs));
let ans_5d = Call::Bid(Bid::new(5, Strain::Diamonds));
let ans_5h = Call::Bid(Bid::new(5, Strain::Hearts));
let ans_5s = Call::Bid(Bid::new(5, Strain::Spades));
let c_5nt = Call::Bid(Bid::new(5, Strain::Notrump));
let extend = |tail: &[Call]| -> Vec<Call> {
our_calls
.iter()
.copied()
.chain(core::iter::once(c_4nt))
.chain(tail.iter().copied())
.collect()
};
insert_uncontested(book, &extend(&[]), rkcb_answers(trump));
let after_5c = Arc::new(asker_after_5c(trump)) as Arc<dyn Classifier>;
let after_5d = Arc::new(asker_after_5d(trump)) as Arc<dyn Classifier>;
let after_5h = Arc::new(asker_after_5h(trump)) as Arc<dyn Classifier>;
let after_5s = Arc::new(asker_after_5s(trump)) as Arc<dyn Classifier>;
let suffix_5c = uncontested(&extend(&[ans_5c]));
let suffix_5d = uncontested(&extend(&[ans_5d]));
let suffix_5h = uncontested(&extend(&[ans_5h]));
let suffix_5s = uncontested(&extend(&[ans_5s]));
insert_arc_all_seats(book, &suffix_5c, 3, Arc::clone(&after_5c));
insert_arc_all_seats(book, &suffix_5d, 3, Arc::clone(&after_5d));
insert_arc_all_seats(book, &suffix_5h, 3, Arc::clone(&after_5h));
insert_arc_all_seats(book, &suffix_5s, 3, Arc::clone(&after_5s));
let shared_king_answers = Arc::new(king_answers(trump)) as Arc<dyn Classifier>;
for &ans in &[ans_5c, ans_5d, ans_5h, ans_5s] {
let king_path = uncontested(&extend(&[ans, c_5nt]));
insert_arc_all_seats(book, &king_path, 3, Arc::clone(&shared_king_answers));
}
let kans_6c = Call::Bid(Bid::new(6, Strain::Clubs));
let kans_6d = Call::Bid(Bid::new(6, Strain::Diamonds));
let kans_6h = Call::Bid(Bid::new(6, Strain::Hearts));
let shared_after_6c = Arc::new(asker_after_6c(trump)) as Arc<dyn Classifier>;
let shared_after_6d = Arc::new(asker_after_6d(trump)) as Arc<dyn Classifier>;
for &ans in &[ans_5c, ans_5d, ans_5h, ans_5s] {
let suffix_6c = uncontested(&extend(&[ans, c_5nt, kans_6c]));
insert_arc_all_seats(book, &suffix_6c, 3, Arc::clone(&shared_after_6c));
let suffix_6d = uncontested(&extend(&[ans, c_5nt, kans_6d]));
insert_arc_all_seats(book, &suffix_6d, 3, Arc::clone(&shared_after_6d));
if trump == Suit::Spades {
let suffix_6h = uncontested(&extend(&[ans, c_5nt, kans_6h]));
let after_6h = Arc::new(asker_after_6h(trump)) as Arc<dyn Classifier>;
insert_arc_all_seats(book, &suffix_6h, 3, after_6h);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bidding::System;
use contract_bridge::auction::RelativeVulnerability;
use contract_bridge::{Hand, Strain};
fn rkcb_trie() -> Trie {
let mut trie = Trie::new();
let our_calls = [
Call::Bid(Bid::new(1, Strain::Spades)),
Call::Bid(Bid::new(2, Strain::Notrump)),
Call::Bid(Bid::new(3, Strain::Clubs)),
];
install_rkcb(&mut trie, &our_calls, Suit::Spades);
trie
}
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 ANS_AUCTION: &[Call] = &[
Call::Bid(Bid::new(1, Strain::Spades)),
Call::Pass,
Call::Bid(Bid::new(2, Strain::Notrump)),
Call::Pass,
Call::Bid(Bid::new(3, Strain::Clubs)),
Call::Pass,
Call::Bid(Bid::new(4, Strain::Notrump)),
Call::Pass,
];
#[test]
fn answers_keycard_counts() {
let trie = rkcb_trie();
assert_eq!(
best(&trie, ANS_AUCTION, "KQ732.K53.Q42.92"),
Call::Bid(Bid::new(5, Strain::Clubs)),
"1 keycard → 5♣"
);
assert_eq!(
best(&trie, ANS_AUCTION, "QJ732.K53.Q42.Q2"),
Call::Bid(Bid::new(5, Strain::Diamonds)),
"0 keycards → 5♦"
);
assert_eq!(
best(&trie, ANS_AUCTION, "AK732.A53.842.92"),
Call::Bid(Bid::new(5, Strain::Diamonds)),
"3 keycards → 5♦"
);
assert_eq!(
best(&trie, ANS_AUCTION, "AQ732.A53.842.92"),
Call::Bid(Bid::new(5, Strain::Spades)),
"2 keycards + trump Q → 5♠"
);
assert_eq!(
best(&trie, ANS_AUCTION, "A8732.A53.842.92"),
Call::Bid(Bid::new(5, Strain::Hearts)),
"2 keycards, no trump Q → 5♥"
);
}
#[test]
fn asker_after_5d_response() {
let trie = rkcb_trie();
let auction: Vec<Call> = ANS_AUCTION
.iter()
.copied()
.chain([Call::Bid(Bid::new(5, Strain::Diamonds)), Call::Pass])
.collect();
assert_eq!(
best(&trie, &auction, "KQ52.AK76.A72.93"),
Call::Bid(Bid::new(5, Strain::Spades)),
"asker with 3 keycards after 5♦ → knows 0, sign off 5♠"
);
assert_eq!(
best(&trie, &auction, "Q852.AK76.K72.A3"),
Call::Bid(Bid::new(6, Strain::Spades)),
"asker with 2 keycards after 5♦ → assumes 3, bid 6♠"
);
}
#[test]
fn king_ask_after_5c() {
let trie = rkcb_trie();
let auction: Vec<Call> = ANS_AUCTION
.iter()
.copied()
.chain([Call::Bid(Bid::new(5, Strain::Clubs)), Call::Pass])
.collect();
assert_eq!(
best(&trie, &auction, "AQ52.A876.A72.A3"),
Call::Bid(Bid::new(5, Strain::Notrump)),
"asker with 4 keycards after 5♣ → 5NT king ask"
);
}
#[test]
fn king_answer_after_5nt() {
let trie = rkcb_trie();
let auction: Vec<Call> = ANS_AUCTION
.iter()
.copied()
.chain([
Call::Bid(Bid::new(5, Strain::Clubs)),
Call::Pass,
Call::Bid(Bid::new(5, Strain::Notrump)),
Call::Pass,
])
.collect();
assert_eq!(
best(&trie, &auction, "K9732.K53.942.92"),
Call::Bid(Bid::new(6, Strain::Diamonds)),
"1 outside king → 6♦"
);
}
}