#![doc = include_str!("../README.md")]
#![warn(missing_docs)]
mod board;
mod ffi;
mod par;
mod play;
mod strain_flags;
mod system_info;
mod tricks;
mod vulnerability;
pub use board::*;
pub use par::*;
pub use play::*;
pub use strain_flags::*;
pub use system_info::*;
pub use tricks::*;
pub use vulnerability::*;
use contract_bridge::deal::FullDeal;
use contract_bridge::seat::Seat;
use dds_bridge_sys as sys;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use core::ffi::c_int;
use core::mem::MaybeUninit;
use core::ptr::NonNull;
use std::sync::LazyLock;
const fn check(status: i32) {
let msg: &[u8] = match status {
0.. => return,
sys::RETURN_ZERO_CARDS => sys::TEXT_ZERO_CARDS,
sys::RETURN_TARGET_TOO_HIGH => sys::TEXT_TARGET_TOO_HIGH,
sys::RETURN_DUPLICATE_CARDS => sys::TEXT_DUPLICATE_CARDS,
sys::RETURN_TARGET_WRONG_LO => sys::TEXT_TARGET_WRONG_LO,
sys::RETURN_TARGET_WRONG_HI => sys::TEXT_TARGET_WRONG_HI,
sys::RETURN_SOLNS_WRONG_LO => sys::TEXT_SOLNS_WRONG_LO,
sys::RETURN_SOLNS_WRONG_HI => sys::TEXT_SOLNS_WRONG_HI,
sys::RETURN_TOO_MANY_CARDS => sys::TEXT_TOO_MANY_CARDS,
sys::RETURN_SUIT_OR_RANK => sys::TEXT_SUIT_OR_RANK,
sys::RETURN_PLAYED_CARD => sys::TEXT_PLAYED_CARD,
sys::RETURN_CARD_COUNT => sys::TEXT_CARD_COUNT,
sys::RETURN_THREAD_INDEX => sys::TEXT_THREAD_INDEX,
sys::RETURN_MODE_WRONG_LO => sys::TEXT_MODE_WRONG_LO,
sys::RETURN_MODE_WRONG_HI => sys::TEXT_MODE_WRONG_HI,
sys::RETURN_TRUMP_WRONG => sys::TEXT_TRUMP_WRONG,
sys::RETURN_FIRST_WRONG => sys::TEXT_FIRST_WRONG,
sys::RETURN_PLAY_FAULT => sys::TEXT_PLAY_FAULT,
sys::RETURN_PBN_FAULT => sys::TEXT_PBN_FAULT,
sys::RETURN_TOO_MANY_BOARDS => sys::TEXT_TOO_MANY_BOARDS,
sys::RETURN_THREAD_CREATE => sys::TEXT_THREAD_CREATE,
sys::RETURN_THREAD_WAIT => sys::TEXT_THREAD_WAIT,
sys::RETURN_THREAD_MISSING => sys::TEXT_THREAD_MISSING,
sys::RETURN_NO_SUIT => sys::TEXT_NO_SUIT,
sys::RETURN_TOO_MANY_TABLES => sys::TEXT_TOO_MANY_TABLES,
sys::RETURN_CHUNK_SIZE => sys::TEXT_CHUNK_SIZE,
_ => sys::TEXT_UNKNOWN_FAULT,
};
panic!("{}", unsafe { core::str::from_utf8_unchecked(msg) });
}
#[must_use]
pub fn calculate_par(tricks: TrickCountTable, vul: Vulnerability, dealer: Seat) -> Par {
let mut par = sys::ParResultsMaster::default();
let status =
unsafe { sys::DealerParBin(&tricks.into(), &raw mut par, vul.to_sys(), dealer as c_int) };
check(status);
par.into()
}
#[must_use]
pub fn calculate_pars(tricks: TrickCountTable, vul: Vulnerability) -> [Par; 2] {
let mut pars = [sys::ParResultsMaster::default(); 2];
let status = unsafe { sys::SidesParBin(&tricks.into(), &raw mut pars[0], vul.to_sys()) };
check(status);
pars.map(Into::into)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TtKind {
Small,
Large,
}
impl TtKind {
#[allow(clippy::cast_possible_wrap)]
const fn to_sys(self) -> c_int {
match self {
Self::Small => sys::DDS_TT_KIND_SMALL as c_int,
Self::Large => sys::DDS_TT_KIND_LARGE as c_int,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SolverConfig {
pub tt_kind: TtKind,
pub tt_mem_default_mb: u32,
pub tt_mem_maximum_mb: u32,
}
impl Default for SolverConfig {
fn default() -> Self {
Self {
tt_kind: TtKind::Large,
tt_mem_default_mb: 0,
tt_mem_maximum_mb: 0,
}
}
}
impl SolverConfig {
fn to_sys(self) -> sys::DdsSolverConfig {
sys::DdsSolverConfig {
tt_kind: self.tt_kind.to_sys(),
tt_mem_default_mb: c_int::try_from(self.tt_mem_default_mb)
.expect("tt_mem_default_mb fits in c_int"),
tt_mem_maximum_mb: c_int::try_from(self.tt_mem_maximum_mb)
.expect("tt_mem_maximum_mb fits in c_int"),
}
}
}
pub struct Solver {
handle: NonNull<sys::DdsSolverContext>,
}
unsafe impl Send for Solver {}
impl Default for Solver {
fn default() -> Self {
Self::new(SolverConfig::default())
}
}
impl Solver {
#[must_use]
pub fn new(config: SolverConfig) -> Self {
let cfg = config.to_sys();
let raw = unsafe { sys::dds_solver_context_new(&raw const cfg) };
let handle = NonNull::new(raw).expect("dds_solver_context_new returned null");
Self { handle }
}
#[must_use]
pub fn solve_deal(&mut self, deal: FullDeal) -> TrickCountTable {
let table_deal = tricks::dd_table_deal_from(deal);
let mut result = sys::DdTableResults::default();
let status = unsafe {
sys::dds_solver_context_reset_for_solve(self.handle.as_ptr());
sys::dds_calc_dd_table(self.handle.as_ptr(), &raw const table_deal, &raw mut result)
};
check(status);
result.into()
}
#[must_use]
pub fn solve_board(&mut self, objective: &Objective) -> FoundPlays {
let deal = sys::Deal::from(objective.board.clone());
let mut result = sys::FutureTricks::default();
let status = unsafe {
sys::dds_solver_context_reset_for_solve(self.handle.as_ptr());
sys::dds_solve_board(
self.handle.as_ptr(),
&raw const deal,
objective.target.target(),
objective.target.solutions(),
0,
&raw mut result,
)
};
check(status);
FoundPlays::from(result)
}
}
impl Drop for Solver {
fn drop(&mut self) {
unsafe { sys::dds_solver_context_free(self.handle.as_ptr()) };
}
}
#[must_use]
pub fn system_info() -> SystemInfo {
let mut inner = MaybeUninit::uninit();
unsafe { sys::GetDDSInfo(inner.as_mut_ptr()) };
SystemInfo(unsafe { inner.assume_init() })
}
#[must_use]
pub fn solve_deals(deals: &[FullDeal]) -> Vec<TrickCountTable> {
if deals.is_empty() {
return Vec::new();
}
let sys_deals: Vec<sys::DdTableDeal> = deals
.iter()
.copied()
.map(tricks::dd_table_deal_from)
.collect();
let mut results: Vec<sys::DdTableResults> = vec![sys::DdTableResults::default(); deals.len()];
let cfg = SolverConfig::default().to_sys();
let status = unsafe {
sys::dds_calc_dd_tables_batched(
c_int::try_from(deals.len()).expect("deals.len() fits in c_int"),
sys_deals.as_ptr(),
results.as_mut_ptr(),
0,
&raw const cfg,
)
};
check(status);
results.into_iter().map(Into::into).collect()
}
#[must_use]
pub fn solve_boards(args: &[Objective]) -> Vec<FoundPlays> {
if args.is_empty() {
return Vec::new();
}
let deals: Vec<sys::Deal> = args
.iter()
.map(|o| sys::Deal::from(o.board.clone()))
.collect();
let targets: Vec<c_int> = args.iter().map(|o| o.target.target()).collect();
let solutions: Vec<c_int> = args.iter().map(|o| o.target.solutions()).collect();
let modes: Vec<c_int> = vec![0; args.len()];
let mut results: Vec<sys::FutureTricks> = vec![sys::FutureTricks::default(); args.len()];
let cfg = SolverConfig::default().to_sys();
let status = unsafe {
sys::dds_solve_boards_batched(
c_int::try_from(args.len()).expect("args.len() fits in c_int"),
deals.as_ptr(),
targets.as_ptr(),
solutions.as_ptr(),
modes.as_ptr(),
results.as_mut_ptr(),
0,
&raw const cfg,
)
};
check(status);
results.into_iter().map(FoundPlays::from).collect()
}
static INIT_LEGACY_POOL: LazyLock<()> = LazyLock::new(|| unsafe { sys::SetMaxThreads(0) });
#[must_use]
pub fn analyse_play(trace: &PlayTrace) -> PlayAnalysis {
LazyLock::force(&INIT_LEGACY_POOL);
let mut result = sys::SolvedPlay::default();
let play = PlayTraceBin::from(&trace.cards);
let status =
unsafe { sys::AnalysePlayBin(trace.board.clone().into(), play.0, &raw mut result, 0) };
check(status);
PlayAnalysis::from(result)
}
#[must_use]
pub fn analyse_plays(traces: &[PlayTrace]) -> Vec<PlayAnalysis> {
traces.par_iter().map(analyse_play).collect()
}