use crate::bidding::{Inferences, sample_layouts};
use crate::stats::HistogramTable;
use contract_bridge::deck::fill_deals;
use contract_bridge::{Builder, Card, FullDeal, Hand, Seat, Strain};
use ddss::{Board, CurrentTrick, NonEmptyStrainFlags, Objective, Play, Solver, Target, TrickCount};
use rand::Rng;
#[must_use]
pub fn single_dummy(
declarer: Hand,
dummy: Hand,
seat: Seat,
rng: &mut impl Rng,
n: usize,
) -> HistogramTable {
let mut builder = Builder::new();
builder[seat] = declarer;
builder[seat.partner()] = dummy;
let partial = builder
.build_partial()
.expect("declarer and dummy must be disjoint thirteen-card hands");
let deals: Vec<_> = fill_deals(rng, partial).take(n).collect();
Solver::lock()
.solve_deals(&deals, NonEmptyStrainFlags::ALL)
.into_iter()
.collect()
}
#[must_use]
pub fn single_dummy_lead_tricks(
deal: &FullDeal,
strain: Strain,
declarer: Seat,
inferences: &Inferences,
rng: &mut impl Rng,
n: usize,
) -> (Card, TrickCount) {
let question = LeadQuestion {
deal: *deal,
strain,
declarer,
inferences: *inferences,
};
single_dummy_leads(&[question], rng, n)
.pop()
.expect("one question yields one answer")
}
#[derive(Clone, Debug)]
pub struct LeadQuestion {
pub deal: FullDeal,
pub strain: Strain,
pub declarer: Seat,
pub inferences: Inferences,
}
#[must_use]
pub fn single_dummy_leads(
questions: &[LeadQuestion],
rng: &mut impl Rng,
n: usize,
) -> Vec<(Card, TrickCount)> {
assert!(n > 0, "a lead needs at least one sampled world");
let mut objectives = Vec::with_capacity(questions.len() * (n + 1));
for question in questions {
let leader = question.declarer.lho();
let hand = question.deal[leader];
let mut worlds = sample_layouts(hand, leader, &question.inferences, rng, n);
if worlds.len() < n {
let mut builder = Builder::new();
builder[leader] = hand;
let partial = builder
.build_partial()
.expect("one thirteen-card hand is a valid partial deal");
worlds.extend(fill_deals(rng, partial).take(n - worlds.len()));
}
worlds.push(question.deal);
objectives.extend(worlds.into_iter().map(|world| {
Objective {
board: Board::try_new(world.into(), CurrentTrick::new(question.strain, leader))
.expect("a full deal at trick one is a valid board"),
target: Target::Legal,
}
}));
}
let found = Solver::lock().solve_boards(&objectives);
let score_of = |plays: &[Play], card: Card| {
plays
.iter()
.find(|play| {
card.suit == play.card.suit
&& (card.rank == play.card.rank || play.equals.contains(card.rank))
})
.map_or(0, |play| u64::from(u8::from(play.score)))
};
questions
.iter()
.zip(found.chunks_exact(n + 1))
.map(|(question, chunk)| {
let (actual, worlds) = chunk.split_last().expect("chunks are n + 1 long");
let hand = question.deal[question.declarer.lho()];
let mut totals: Vec<(Card, u64)> = hand.into_iter().map(|card| (card, 0)).collect();
for world in worlds {
for (card, total) in &mut totals {
*total += score_of(&world.plays, *card);
}
}
let (lead, _) = totals
.into_iter()
.max_by_key(|&(_, total)| total)
.expect("the leader holds thirteen cards");
#[allow(clippy::cast_possible_truncation)]
let tricks = TrickCount::try_new(13 - score_of(&actual.plays, lead) as u8)
.expect("13 minus a trick count is a trick count");
(lead, tricks)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use contract_bridge::{Bid, Level, Strain};
use rand::SeedableRng;
use rand::rngs::StdRng;
const fn four_spades() -> Bid {
Bid {
level: Level::new(4),
strain: Strain::Spades,
}
}
fn unbeatable_spade_fit() -> (Hand, Hand) {
let north: Hand = "AKQJT98.AK.AK.AK".parse().expect("valid test hand");
let south: Hand = "765432.QJT.QJ.QJ".parse().expect("valid test hand");
(north, south)
}
#[test]
fn unbeatable_fit_always_makes() {
let (north, south) = unbeatable_spade_fit();
let mut rng = StdRng::seed_from_u64(1);
let hist = single_dummy(north, south, Seat::North, &mut rng, 16);
assert_eq!(hist.expected_tricks(Seat::North, Strain::Spades), 13.0);
assert_eq!(hist.make_probability(Seat::North, four_spades()), 1.0);
assert_eq!(hist.make_probability(Seat::East, four_spades()), 0.0);
}
#[test]
fn deterministic_given_a_seed() {
let (north, south) = unbeatable_spade_fit();
let mut rng_a = StdRng::seed_from_u64(7);
let a = single_dummy(north, south, Seat::North, &mut rng_a, 12);
let mut rng_b = StdRng::seed_from_u64(7);
let b = single_dummy(north, south, Seat::North, &mut rng_b, 12);
assert_eq!(a, b);
}
#[test]
fn empty_is_no_signal() {
let (north, south) = unbeatable_spade_fit();
let mut rng = StdRng::seed_from_u64(0);
let hist = single_dummy(north, south, Seat::North, &mut rng, 0);
assert!(hist.expected_tricks(Seat::North, Strain::Spades).is_nan());
assert!(hist.make_probability(Seat::North, four_spades()).is_nan());
}
fn unbeatable_deal() -> FullDeal {
let (north, south) = unbeatable_spade_fit();
let east: Hand = ".987654.T9876.T9".parse().expect("valid test hand");
let west: Hand = ".32.5432.8765432".parse().expect("valid test hand");
let mut builder = Builder::new();
builder[Seat::North] = north;
builder[Seat::South] = south;
builder[Seat::East] = east;
builder[Seat::West] = west;
builder.build_full().expect("52 disjoint cards")
}
fn no_inferences() -> Inferences {
use crate::bidding::Context;
use contract_bridge::auction::RelativeVulnerability;
Inferences::read(&Context::new(RelativeVulnerability::NONE, &[]))
}
#[test]
fn lead_cannot_beat_the_lock() {
let deal = unbeatable_deal();
let mut rng = StdRng::seed_from_u64(3);
let (lead, tricks) = single_dummy_lead_tricks(
&deal,
Strain::Spades,
Seat::North,
&no_inferences(),
&mut rng,
8,
);
assert!(deal[Seat::East][lead.suit].contains(lead.rank));
assert_eq!(u8::from(tricks), 13);
}
#[test]
fn lead_choice_is_deterministic() {
let deal = unbeatable_deal();
let inferences = no_inferences();
let mut rng_a = StdRng::seed_from_u64(11);
let a = single_dummy_lead_tricks(
&deal,
Strain::Spades,
Seat::North,
&inferences,
&mut rng_a,
6,
);
let mut rng_b = StdRng::seed_from_u64(11);
let b = single_dummy_lead_tricks(
&deal,
Strain::Spades,
Seat::North,
&inferences,
&mut rng_b,
6,
);
assert_eq!(a, b);
}
}