use super::Rules;
use super::array::Logits;
use super::context::Context;
use super::instinct::{forced, instinct};
use super::trie::Classifier;
use super::{features, neural};
use contract_bridge::Hand;
use contract_bridge::auction::{Auction, Call};
use std::sync::LazyLock;
static LADDER: LazyLock<Rules> = LazyLock::new(instinct);
#[derive(Clone, Copy, Debug, Default)]
pub struct NeuralFloor;
impl Classifier for NeuralFloor {
fn classify(&self, hand: Hand, context: &Context<'_>) -> Logits {
if forced(context) {
return LADDER.classify(hand, context);
}
let mut logits = neural::classify(&features::features(hand, context));
mask_illegal(&mut logits, context.auction());
logits
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct NeuralFloorV2;
impl Classifier for NeuralFloorV2 {
fn classify(&self, hand: Hand, context: &Context<'_>) -> Logits {
if forced(context) {
return LADDER.classify(hand, context);
}
let mut logits = neural::classify_v2(&features::features_v2(hand, context));
mask_illegal(&mut logits, context.auction());
logits
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct NeuralFloorSearch;
impl Classifier for NeuralFloorSearch {
fn classify(&self, hand: Hand, context: &Context<'_>) -> Logits {
if forced(context) {
return LADDER.classify(hand, context);
}
let mut logits = neural::classify_search(&features::features(hand, context));
mask_illegal(&mut logits, context.auction());
logits
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct NeuralFloorV3;
impl Classifier for NeuralFloorV3 {
fn classify(&self, hand: Hand, context: &Context<'_>) -> Logits {
if forced(context) {
return LADDER.classify(hand, context);
}
let mut logits = neural::classify_v3(&features::features_v3(hand, context));
mask_illegal(&mut logits, context.auction());
logits
}
}
pub(crate) fn mask_illegal(logits: &mut Logits, auction: &[Call]) {
let mut played = Auction::new();
played
.try_extend(auction.iter().copied())
.expect("a prior table auction is legal");
for (call, slot) in logits.iter_mut() {
if played.can_push(call).is_err() {
*slot = f32::NEG_INFINITY;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use contract_bridge::auction::RelativeVulnerability;
use contract_bridge::{Bid, Strain};
const fn call(level: u8, strain: Strain) -> Call {
Call::Bid(Bid::new(level, strain))
}
fn shelled(auction: &[Call], hand: &str) -> Logits {
let hand: Hand = hand.parse().expect("valid test hand");
let context = Context::new(RelativeVulnerability::NONE, auction);
NeuralFloor.classify(hand, &context)
}
fn best(auction: &[Call], hand: &str) -> Call {
let logits = shelled(auction, hand);
(&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")
}
fn best_v2(auction: &[Call], hand: &str) -> Call {
let hand: Hand = hand.parse().expect("valid test hand");
let context = Context::new(RelativeVulnerability::NONE, auction);
let logits = NeuralFloorV2.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")
}
#[test]
fn advancing_a_double_delegates_to_instinct() {
let auction = [call(3, Strain::Clubs), Call::Double, Call::Pass];
assert_eq!(best(&auction, "96432.J85.9742.2"), call(3, Strain::Spades));
assert_eq!(best(&auction, "964.J85.974.9632"), Call::Pass);
}
#[test]
fn forced_to_game_never_passes_below_game() {
let auction = [
call(2, Strain::Clubs),
Call::Pass,
call(2, Strain::Diamonds),
Call::Pass,
call(2, Strain::Notrump),
Call::Pass,
];
assert_eq!(best(&auction, "QJ52.K43.T62.J32"), call(3, Strain::Notrump));
assert_eq!(best(&auction, "3.QJ9854.K32.J32"), call(4, Strain::Hearts));
}
#[test]
fn completes_partners_transfer_over_notrump() {
let auction = [
call(1, Strain::Notrump),
Call::Pass,
call(2, Strain::Diamonds),
Call::Pass,
];
assert_eq!(best(&auction, "AQ32.KJ5.KQ4.Q92"), call(2, Strain::Hearts));
}
#[test]
fn forced_game_steps_aside_when_penalizing() {
let auction = [
call(2, Strain::Clubs),
Call::Pass,
call(2, Strain::Diamonds),
Call::Pass,
call(2, Strain::Notrump),
call(3, Strain::Diamonds),
Call::Double,
Call::Pass,
];
let chosen = best(&auction, "K3.KQ4.65.QJ8765");
assert_eq!(chosen, call(4, Strain::Clubs));
assert_ne!(chosen, call(3, Strain::Notrump));
}
#[test]
fn doubles_only_their_live_bids() {
let auction = [
call(1, Strain::Hearts),
call(1, Strain::Spades),
Call::Pass,
call(2, Strain::Spades),
Call::Pass,
];
let logits = shelled(&auction, "92.K53.AQJ42.962");
assert_eq!(*logits.0.get(Call::Double), f32::NEG_INFINITY);
assert!(logits.0.get(Call::Pass).is_finite());
}
#[test]
fn v2_advancing_a_double_advances_a_bust() {
let auction = [call(3, Strain::Clubs), Call::Double, Call::Pass];
assert_eq!(
best_v2(&auction, "96432.J85.9742.2"),
call(3, Strain::Spades)
);
}
#[test]
fn v2_masks_illegal_keeps_pass_finite() {
let auction = [
call(1, Strain::Hearts),
call(1, Strain::Spades),
Call::Pass,
call(2, Strain::Spades),
Call::Pass,
];
let hand: Hand = "92.K53.AQJ42.962".parse().unwrap();
let context = Context::new(RelativeVulnerability::NONE, &auction);
let logits = NeuralFloorV2.classify(hand, &context);
assert_eq!(*logits.0.get(Call::Double), f32::NEG_INFINITY);
assert!(logits.0.get(Call::Pass).is_finite());
}
#[test]
fn search_advancing_a_double_advances_a_bust() {
let auction = [call(3, Strain::Clubs), Call::Double, Call::Pass];
let hand: Hand = "96432.J85.9742.2".parse().unwrap();
let context = Context::new(RelativeVulnerability::NONE, &auction);
let chosen = NeuralFloorSearch
.classify(hand, &context)
.0
.into_iter()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).expect("logits are never NaN"))
.map(|(call, _)| call)
.expect("array is never empty");
assert_eq!(chosen, call(3, Strain::Spades));
}
#[test]
fn search_masks_illegal_keeps_pass_finite() {
let auction = [
call(1, Strain::Hearts),
call(1, Strain::Spades),
Call::Pass,
call(2, Strain::Spades),
Call::Pass,
];
let hand: Hand = "92.K53.AQJ42.962".parse().unwrap();
let context = Context::new(RelativeVulnerability::NONE, &auction);
let logits = NeuralFloorSearch.classify(hand, &context);
assert_eq!(*logits.0.get(Call::Double), f32::NEG_INFINITY);
assert!(logits.0.get(Call::Pass).is_finite());
}
#[test]
fn v3_advancing_a_double_advances_a_bust() {
let auction = [call(3, Strain::Clubs), Call::Double, Call::Pass];
let hand: Hand = "96432.J85.9742.2".parse().unwrap();
let context = Context::new(RelativeVulnerability::NONE, &auction);
let chosen = NeuralFloorV3
.classify(hand, &context)
.0
.into_iter()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).expect("logits are never NaN"))
.map(|(call, _)| call)
.expect("array is never empty");
assert_eq!(chosen, call(3, Strain::Spades));
}
#[test]
fn v3_masks_illegal_keeps_pass_finite() {
let auction = [
call(1, Strain::Hearts),
call(1, Strain::Spades),
Call::Pass,
call(2, Strain::Spades),
Call::Pass,
];
let hand: Hand = "92.K53.AQJ42.962".parse().unwrap();
let context = Context::new(RelativeVulnerability::NONE, &auction);
let logits = NeuralFloorV3.classify(hand, &context);
assert_eq!(*logits.0.get(Call::Double), f32::NEG_INFINITY);
assert!(logits.0.get(Call::Pass).is_finite());
}
}