use super::System;
use super::constraint::point_count;
use super::inference::{Inference, Inferences, Relative, relative_of};
use contract_bridge::auction::{Auction, Call, RelativeVulnerability};
use contract_bridge::deck::fill_deals;
use contract_bridge::{Builder, FullDeal, Hand, Seat, Suit};
use rand::Rng;
const MAX_ATTEMPTS_PER_LAYOUT: usize = 256;
const REPLAY_DRAW_CAP: usize = 50_000_000;
const REPLAY_DRY_LIMIT: usize = 1 << 20;
const MARGIN: f32 = 3.0;
#[must_use]
pub fn sample_layouts(
hand: Hand,
seat: Seat,
inferences: &Inferences,
rng: &mut impl Rng,
n: usize,
) -> Vec<FullDeal> {
let budget = n.saturating_mul(MAX_ATTEMPTS_PER_LAYOUT);
sample_with(hand, seat, rng, n, budget, usize::MAX, |deal| {
within_ranges(deal, seat, inferences)
})
}
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn sample_layouts_replay(
hand: Hand,
seat: Seat,
policy: &dyn System,
vul: RelativeVulnerability,
auction: &[Call],
inferences: &Inferences,
rng: &mut impl Rng,
n: usize,
) -> Vec<FullDeal> {
sample_with(
hand,
seat,
rng,
n,
REPLAY_DRAW_CAP,
REPLAY_DRY_LIMIT,
|deal| {
within_ranges(deal, seat, inferences) && rules_accept(deal, seat, policy, vul, auction)
},
)
}
fn sample_with(
hand: Hand,
seat: Seat,
rng: &mut impl Rng,
n: usize,
budget: usize,
dry_limit: usize,
accept: impl Fn(&FullDeal) -> bool,
) -> Vec<FullDeal> {
let mut out = Vec::with_capacity(n);
if n == 0 {
return out;
}
let mut builder = Builder::new();
builder[seat] = hand;
let partial = builder
.build_partial()
.expect("one thirteen-card hand is a valid partial deal");
let mut dry = 0usize;
for deal in fill_deals(rng, partial).take(budget) {
if accept(&deal) {
out.push(deal);
if out.len() == n {
break;
}
dry = 0;
} else {
dry += 1;
if dry >= dry_limit {
break;
}
}
}
out
}
fn within_ranges(deal: &FullDeal, seat: Seat, inferences: &Inferences) -> bool {
[
(seat.lho(), inferences.lho()),
(seat.partner(), inferences.partner()),
(seat.rho(), inferences.rho()),
]
.into_iter()
.all(|(other, shown)| hand_within(deal[other], shown))
}
fn hand_within(hand: Hand, shown: &Inference) -> bool {
let lengths_fit = Suit::ASC.into_iter().all(|suit| {
#[allow(clippy::cast_possible_truncation)]
let length = hand[suit].len() as u8;
shown.length(suit).contains(length)
});
lengths_fit && shown.points.contains(point_count(hand))
}
fn rules_accept(
deal: &FullDeal,
seat: Seat,
policy: &dyn System,
vul: RelativeVulnerability,
auction: &[Call],
) -> bool {
let len = auction.len();
let theirs = swap_sides(vul);
[
(seat.lho(), Relative::Lho, theirs),
(seat.partner(), Relative::Partner, vul),
(seat.rho(), Relative::Rho, theirs),
]
.into_iter()
.all(|(other, who, pvul)| {
let hand = deal[other];
(0..len)
.rev()
.filter(|&i| relative_of(len, i) == who)
.all(|i| made_plausibly(hand, policy, pvul, &auction[..i], auction[i]))
})
}
fn made_plausibly(
hand: Hand,
policy: &dyn System,
vul: RelativeVulnerability,
prefix: &[Call],
made: Call,
) -> bool {
if matches!(made, Call::Pass) || !policy.authored_at(vul, prefix) {
return true;
}
let Some(logits) = policy.classify(hand, vul, prefix) else {
return true;
};
let mut played = Auction::new();
played
.try_extend(prefix.iter().copied())
.expect("a prior table auction is legal");
let best = logits
.0
.iter()
.filter(|(call, _)| played.can_push(*call).is_ok())
.fold(f32::NEG_INFINITY, |best, (_, &logit)| best.max(logit));
*logits.0.get(made) >= best - MARGIN
}
fn swap_sides(vul: RelativeVulnerability) -> RelativeVulnerability {
let mut out = RelativeVulnerability::NONE;
out.set(
RelativeVulnerability::WE,
vul.contains(RelativeVulnerability::THEY),
);
out.set(
RelativeVulnerability::THEY,
vul.contains(RelativeVulnerability::WE),
);
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bidding::context::Context;
use contract_bridge::auction::{Call, RelativeVulnerability};
use contract_bridge::deck::full_deal;
use contract_bridge::{Bid, Level, Strain};
use proptest::prelude::*;
use rand::SeedableRng;
use rand::rngs::StdRng;
const fn bid(level: u8, strain: Strain) -> Call {
Call::Bid(Bid {
level: Level::new(level),
strain,
})
}
fn inferences(auction: &[Call]) -> Inferences {
Inferences::read(&Context::new(RelativeVulnerability::NONE, auction))
}
#[test]
fn reads_natural_penalty_double_of_their_notrump() {
let direct = inferences(&[bid(1, Strain::Notrump), Call::Double]);
assert_eq!(direct.rho().points.min, 15);
let passed = inferences(&[
Call::Pass,
bid(1, Strain::Notrump),
Call::Pass,
Call::Pass,
Call::Double,
]);
assert!(passed.rho().points.min < 15);
}
#[test]
fn reads_latched_penalty_double_of_the_runout() {
use crate::bidding::instinct::set_penalty_latch;
let auction = [
bid(1, Strain::Notrump),
Call::Double,
bid(2, Strain::Diamonds),
Call::Double,
Call::Pass,
];
set_penalty_latch(false);
assert_eq!(inferences(&auction).partner().length(Suit::Diamonds).min, 0);
set_penalty_latch(true);
assert_eq!(inferences(&auction).partner().length(Suit::Diamonds).min, 4);
}
fn short_heart_actor() -> Hand {
"AKQ32.32.AKQ2.32".parse().expect("valid test hand")
}
#[test]
fn sampled_layouts_respect_ranges() {
let actor = Seat::North;
let inf = inferences(&[bid(1, Strain::Hearts)]);
proptest!(|(seed in any::<u64>())| {
let mut rng = StdRng::seed_from_u64(seed);
let hand = full_deal(&mut rng)[actor];
for deal in sample_layouts(hand, actor, &inf, &mut rng, 4) {
prop_assert_eq!(deal[actor], hand);
for (other, shown) in [
(actor.lho(), inf.lho()),
(actor.partner(), inf.partner()),
(actor.rho(), inf.rho()),
] {
for suit in Suit::ASC {
#[allow(clippy::cast_possible_truncation)]
let length = deal[other][suit].len() as u8;
prop_assert!(shown.length(suit).contains(length));
}
prop_assert!(shown.points.contains(point_count(deal[other])));
}
}
});
}
#[test]
fn respects_a_developed_auction() {
let actor = Seat::North;
let auction = [bid(1, Strain::Hearts), bid(1, Strain::Spades)];
let inf = inferences(&auction);
let mut rng = StdRng::seed_from_u64(7);
let layouts = sample_layouts(short_heart_actor(), actor, &inf, &mut rng, 16);
assert!(!layouts.is_empty(), "the auction is feasible");
for deal in &layouts {
let partner = deal[actor.partner()];
assert!(partner[Suit::Hearts].len() >= 5);
assert!(inf.partner().points.contains(point_count(partner)));
let rho = deal[actor.rho()];
assert!(rho[Suit::Spades].len() >= 5);
assert!(inf.rho().points.contains(point_count(rho)));
}
}
#[test]
fn opener_constraint_is_enforced() {
let actor = Seat::North;
let inf = inferences(&[bid(1, Strain::Hearts)]);
let mut rng = StdRng::seed_from_u64(1);
let layouts = sample_layouts(short_heart_actor(), actor, &inf, &mut rng, 32);
assert_eq!(layouts.len(), 32, "a 1H opening is easy to satisfy");
for deal in &layouts {
let opener = deal[actor.rho()];
assert!(opener[Suit::Hearts].len() >= 5);
assert!((10..=21).contains(&point_count(opener)));
}
}
#[test]
fn coverage_is_not_degenerate() {
let actor = Seat::North;
let inf = inferences(&[bid(1, Strain::Hearts)]);
let mut rng = StdRng::seed_from_u64(99);
let layouts = sample_layouts(short_heart_actor(), actor, &inf, &mut rng, 40);
let rho_hearts: std::collections::HashSet<usize> = layouts
.iter()
.map(|deal| deal[actor.rho()][Suit::Hearts].len())
.collect();
let lho_spades: std::collections::HashSet<usize> = layouts
.iter()
.map(|deal| deal[actor.lho()][Suit::Spades].len())
.collect();
assert!(rho_hearts.len() >= 2, "constrained seat should still vary");
assert!(lho_spades.len() >= 3, "free seat should vary widely");
}
#[test]
fn infeasible_auction_returns_empty() {
let actor = Seat::North;
let inf = inferences(&[bid(1, Strain::Hearts)]);
let hoard: Hand = "32.AKQJT9876.2.2".parse().expect("valid test hand");
assert_eq!(hoard[Suit::Hearts].len(), 9);
let mut rng = StdRng::seed_from_u64(5);
let layouts = sample_layouts(hoard, actor, &inf, &mut rng, 5);
assert!(layouts.is_empty());
}
#[test]
fn zero_request_is_empty() {
let actor = Seat::North;
let inf = inferences(&[bid(1, Strain::Hearts)]);
let mut rng = StdRng::seed_from_u64(0);
assert!(sample_layouts(short_heart_actor(), actor, &inf, &mut rng, 0).is_empty());
}
#[test]
fn replay_honors_both_sides_under_competition() {
let policy = crate::american().against(crate::bidding::Family::NATURAL);
let actor = Seat::North;
let auction = [bid(1, Strain::Hearts), bid(2, Strain::Clubs)];
let inf = inferences(&auction);
let mut rng = StdRng::seed_from_u64(3);
let layouts = sample_layouts_replay(
short_heart_actor(),
actor,
&policy,
RelativeVulnerability::NONE,
&auction,
&inf,
&mut rng,
16,
);
assert!(!layouts.is_empty(), "the auction is feasible");
for deal in &layouts {
assert!(
deal[actor.partner()][Suit::Hearts].len() >= 5,
"partner's 1H opening promises 5+ hearts"
);
assert!(
deal[actor.rho()][Suit::Clubs].len() >= 5,
"RHO's 2C overcall promises 5+ clubs"
);
}
}
}