use super::System;
use super::context::Context;
use super::inference::{Inferences, rule_accept_enabled};
use super::sampler::{sample_layouts, sample_layouts_replay};
use super::table::Table;
use crate::scoring::{final_contract, ns_score_bid};
use contract_bridge::auction::{Auction, Call};
use contract_bridge::{AbsoluteVulnerability, Hand, Seat};
use ddss::{NonEmptyStrainFlags, Solver};
use rand::Rng;
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn ev(
hand: Hand,
seat: Seat,
vul: AbsoluteVulnerability,
context: &Context<'_>,
call: Call,
policy: &impl System,
rng: &mut impl Rng,
n: usize,
) -> f32 {
ev_all(hand, seat, vul, context, &[call], policy, rng, n)[0]
}
#[must_use]
#[allow(clippy::cast_precision_loss)] #[allow(clippy::too_many_arguments)] pub fn ev_all(
hand: Hand,
seat: Seat,
vul: AbsoluteVulnerability,
context: &Context<'_>,
calls: &[Call],
policy: &impl System,
rng: &mut impl Rng,
n: usize,
) -> Vec<f32> {
if calls.is_empty() {
return Vec::new();
}
let inferences = Inferences::read(context);
let deals = if rule_accept_enabled() {
let mut deals = sample_layouts_replay(
hand,
seat,
policy,
context.vul(),
context.auction(),
&inferences,
rng,
n,
);
if deals.len() < n {
let more = sample_layouts(hand, seat, &inferences, rng, n - deals.len());
deals.extend(more);
}
deals
} else {
sample_layouts(hand, seat, &inferences, rng, n)
};
if deals.is_empty() {
return vec![f32::NAN; calls.len()];
}
let tables = Solver::lock().solve_deals(&deals, NonEmptyStrainFlags::ALL);
let dealer = dealer_of(seat, context.auction().len());
let table = Table::new(policy, policy, dealer, vul);
let actor_is_ns = matches!(seat, Seat::North | Seat::South);
calls
.iter()
.map(|&call| {
let mut seed = Auction::new();
seed.try_extend(context.auction().iter().copied())
.expect("a prior table auction is legal");
if seed.can_push(call).is_err() {
return f32::NAN;
}
seed.push(call);
let total: i64 = deals
.iter()
.zip(tables.iter())
.map(|(deal, tricks)| {
let auction = table.bid_out_from(deal, seed.clone());
let result = final_contract(&auction, dealer).map(|(c, s)| (c.bid, s));
let score = ns_score_bid(result, tricks, vul);
if actor_is_ns { score } else { -score }
})
.sum();
total as f32 / deals.len() as f32
})
.collect()
}
fn dealer_of(seat: Seat, prior_len: usize) -> Seat {
let actor = Seat::ALL
.iter()
.position(|&s| s == seat)
.expect("every seat is in Seat::ALL");
Seat::ALL[(actor + 4 - prior_len % 4) % 4]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::american;
use crate::bidding::Family;
use contract_bridge::auction::RelativeVulnerability;
use contract_bridge::{Bid, Level, Strain};
use rand::SeedableRng;
use rand::rngs::StdRng;
const fn bid(level: u8, strain: Strain) -> Call {
Call::Bid(Bid {
level: Level::new(level),
strain,
})
}
fn balanced_twenty() -> Hand {
"AKQ2.KQ2.KJ2.Q32".parse().expect("valid test hand")
}
fn deterministic() -> impl System {
american().against(Family::NATURAL)
}
#[test]
fn prefers_game_over_hopeless_grand() {
let policy = deterministic();
let context = Context::new(RelativeVulnerability::NONE, &[]);
let mut rng = StdRng::seed_from_u64(20);
let evs = ev_all(
balanced_twenty(),
Seat::North,
AbsoluteVulnerability::NONE,
&context,
&[bid(3, Strain::Notrump), bid(7, Strain::Notrump)],
&policy,
&mut rng,
48,
);
assert!(
evs[0] > evs[1],
"3NT ({}) should beat 7NT ({})",
evs[0],
evs[1]
);
assert!(
evs[1] < 0.0,
"7NT off the top should be negative, got {}",
evs[1]
);
}
#[test]
fn deterministic_given_a_seed() {
let policy = deterministic();
let context = Context::new(RelativeVulnerability::NONE, &[]);
let calls = [bid(3, Strain::Notrump), Call::Pass];
let mut rng_a = StdRng::seed_from_u64(7);
let a = ev_all(
balanced_twenty(),
Seat::North,
AbsoluteVulnerability::NONE,
&context,
&calls,
&policy,
&mut rng_a,
24,
);
let mut rng_b = StdRng::seed_from_u64(7);
let b = ev_all(
balanced_twenty(),
Seat::North,
AbsoluteVulnerability::NONE,
&context,
&calls,
&policy,
&mut rng_b,
24,
);
assert_eq!(a, b);
}
#[test]
fn infeasible_auction_is_no_signal() {
let policy = deterministic();
let auction = [bid(1, Strain::Hearts)];
let context = Context::new(RelativeVulnerability::NONE, &auction);
let hoard: Hand = "32.AKQJT9876.2.2".parse().expect("valid test hand");
let mut rng = StdRng::seed_from_u64(5);
let evs = ev_all(
hoard,
Seat::North,
AbsoluteVulnerability::NONE,
&context,
&[Call::Pass, bid(2, Strain::Hearts)],
&policy,
&mut rng,
8,
);
assert!(
evs.iter().all(|ev| ev.is_nan()),
"no layout means no signal"
);
}
#[test]
fn illegal_candidate_is_nan() {
let policy = deterministic();
let auction = [bid(1, Strain::Hearts)];
let context = Context::new(RelativeVulnerability::NONE, &auction);
let mut rng = StdRng::seed_from_u64(3);
let evs = ev_all(
balanced_twenty(),
Seat::North,
AbsoluteVulnerability::NONE,
&context,
&[bid(1, Strain::Clubs), Call::Pass],
&policy,
&mut rng,
8,
);
assert!(evs[0].is_nan(), "1C over 1H is illegal");
assert!(evs[1].is_finite(), "Pass is legal and should score");
}
#[test]
fn empty_candidates_is_empty() {
let policy = deterministic();
let context = Context::new(RelativeVulnerability::NONE, &[]);
let mut rng = StdRng::seed_from_u64(0);
assert!(
ev_all(
balanced_twenty(),
Seat::North,
AbsoluteVulnerability::NONE,
&context,
&[],
&policy,
&mut rng,
8,
)
.is_empty()
);
}
}