#[cfg(test)]
mod test;
use crate::{Deal, Seat, Strain};
use core::ffi::c_int;
use core::fmt;
use core::num::Wrapping;
use core::ops::BitOr as _;
use dds_bridge_sys as sys;
use once_cell::sync::Lazy;
use std::sync::{Mutex, PoisonError};
use thiserror::Error;
static THREAD_POOL: Lazy<Mutex<()>> = Lazy::new(|| {
unsafe { sys::SetMaxThreads(0) };
Mutex::new(())
});
#[derive(Debug, Error, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(i32)]
pub enum SystemError {
#[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_NO_FAULT) })]
#[allow(clippy::cast_possible_wrap)]
Success = sys::RETURN_NO_FAULT as i32,
#[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_UNKNOWN_FAULT) })]
UnknownFault = sys::RETURN_UNKNOWN_FAULT,
#[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_ZERO_CARDS) })]
ZeroCards = sys::RETURN_ZERO_CARDS,
#[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_TARGET_TOO_HIGH) })]
TargetTooHigh = sys::RETURN_TARGET_TOO_HIGH,
#[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_DUPLICATE_CARDS) })]
DuplicateCards = sys::RETURN_DUPLICATE_CARDS,
#[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_TARGET_WRONG_LO) })]
NegativeTarget = sys::RETURN_TARGET_WRONG_LO,
#[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_TARGET_WRONG_HI) })]
InvalidTarget = sys::RETURN_TARGET_WRONG_HI,
#[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_SOLNS_WRONG_LO) })]
LowSolvingParameter = sys::RETURN_SOLNS_WRONG_LO,
#[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_SOLNS_WRONG_HI) })]
HighSolvingParameter = sys::RETURN_SOLNS_WRONG_HI,
#[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_TOO_MANY_CARDS) })]
TooManyCards = sys::RETURN_TOO_MANY_CARDS,
#[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_SUIT_OR_RANK) })]
CurrentSuitOrRank = sys::RETURN_SUIT_OR_RANK,
#[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_PLAYED_CARD) })]
PlayedCard = sys::RETURN_PLAYED_CARD,
#[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_CARD_COUNT) })]
CardCount = sys::RETURN_CARD_COUNT,
#[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_THREAD_INDEX) })]
ThreadIndex = sys::RETURN_THREAD_INDEX,
#[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_MODE_WRONG_LO) })]
NegativeModeParameter = sys::RETURN_MODE_WRONG_LO,
#[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_MODE_WRONG_HI) })]
HighModeParameter = sys::RETURN_MODE_WRONG_HI,
#[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_TRUMP_WRONG) })]
Trump = sys::RETURN_TRUMP_WRONG,
#[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_FIRST_WRONG) })]
First = sys::RETURN_FIRST_WRONG,
#[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_PLAY_FAULT) })]
AnalysePlay = sys::RETURN_PLAY_FAULT,
#[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_PBN_FAULT) })]
PBN = sys::RETURN_PBN_FAULT,
#[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_TOO_MANY_BOARDS) })]
TooManyBoards = sys::RETURN_TOO_MANY_BOARDS,
#[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_THREAD_CREATE) })]
ThreadCreate = sys::RETURN_THREAD_CREATE,
#[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_THREAD_WAIT) })]
ThreadWait = sys::RETURN_THREAD_WAIT,
#[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_THREAD_MISSING) })]
ThreadMissing = sys::RETURN_THREAD_MISSING,
#[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_NO_SUIT) })]
NoSuit = sys::RETURN_NO_SUIT,
#[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_TOO_MANY_TABLES) })]
TooManyTables = sys::RETURN_TOO_MANY_TABLES,
#[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_CHUNK_SIZE) })]
ChunkSize = sys::RETURN_CHUNK_SIZE,
}
impl SystemError {
pub const fn propagate<T: Copy>(x: T, status: i32) -> Result<T, Self> {
match status {
0.. => Ok(x),
sys::RETURN_ZERO_CARDS => Err(Self::ZeroCards),
sys::RETURN_TARGET_TOO_HIGH => Err(Self::TargetTooHigh),
sys::RETURN_DUPLICATE_CARDS => Err(Self::DuplicateCards),
sys::RETURN_TARGET_WRONG_LO => Err(Self::NegativeTarget),
sys::RETURN_TARGET_WRONG_HI => Err(Self::InvalidTarget),
sys::RETURN_SOLNS_WRONG_LO => Err(Self::LowSolvingParameter),
sys::RETURN_SOLNS_WRONG_HI => Err(Self::HighSolvingParameter),
sys::RETURN_TOO_MANY_CARDS => Err(Self::TooManyCards),
sys::RETURN_SUIT_OR_RANK => Err(Self::CurrentSuitOrRank),
sys::RETURN_PLAYED_CARD => Err(Self::PlayedCard),
sys::RETURN_CARD_COUNT => Err(Self::CardCount),
sys::RETURN_THREAD_INDEX => Err(Self::ThreadIndex),
sys::RETURN_MODE_WRONG_LO => Err(Self::NegativeModeParameter),
sys::RETURN_MODE_WRONG_HI => Err(Self::HighModeParameter),
sys::RETURN_TRUMP_WRONG => Err(Self::Trump),
sys::RETURN_FIRST_WRONG => Err(Self::First),
sys::RETURN_PLAY_FAULT => Err(Self::AnalysePlay),
sys::RETURN_PBN_FAULT => Err(Self::PBN),
sys::RETURN_TOO_MANY_BOARDS => Err(Self::TooManyBoards),
sys::RETURN_THREAD_CREATE => Err(Self::ThreadCreate),
sys::RETURN_THREAD_WAIT => Err(Self::ThreadWait),
sys::RETURN_THREAD_MISSING => Err(Self::ThreadMissing),
sys::RETURN_NO_SUIT => Err(Self::NoSuit),
sys::RETURN_TOO_MANY_TABLES => Err(Self::TooManyTables),
sys::RETURN_CHUNK_SIZE => Err(Self::ChunkSize),
_ => Err(Self::UnknownFault),
}
}
}
#[derive(Debug, Error)]
pub enum Error {
#[error(transparent)]
System(SystemError),
#[error("The thread pool is poisoned")]
Poison,
}
impl From<SystemError> for Error {
fn from(err: SystemError) -> Self {
Self::System(err)
}
}
impl<T> From<PoisonError<T>> for Error {
fn from(_: PoisonError<T>) -> Self {
Self::Poison
}
}
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct StrainFlags : u8 {
const CLUBS = 0x01;
const DIAMONDS = 0x02;
const HEARTS = 0x04;
const SPADES = 0x08;
const NOTRUMP = 0x10;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TricksRow(u16);
impl TricksRow {
#[must_use]
pub const fn new(n: u8, e: u8, s: u8, w: u8) -> Self {
Self(
(n as u16) << (4 * Seat::North as u8)
| (e as u16) << (4 * Seat::East as u8)
| (s as u16) << (4 * Seat::South as u8)
| (w as u16) << (4 * Seat::West as u8),
)
}
#[must_use]
pub const fn get(self, seat: Seat) -> u8 {
(self.0 >> (4 * seat as u8) & 0xF) as u8
}
#[must_use]
pub fn hex(self, seat: Seat) -> impl fmt::UpperHex {
TricksRowHex { deal: self, seat }
}
}
struct TricksRowHex {
deal: TricksRow,
seat: Seat,
}
impl fmt::UpperHex for TricksRowHex {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{:X}{:X}{:X}{:X}",
self.deal.get(self.seat),
self.deal.get(self.seat + Wrapping(1)),
self.deal.get(self.seat + Wrapping(2)),
self.deal.get(self.seat + Wrapping(3)),
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TricksTable(pub [TricksRow; 5]);
impl core::ops::Index<Strain> for TricksTable {
type Output = TricksRow;
fn index(&self, strain: Strain) -> &TricksRow {
&self.0[strain as usize]
}
}
struct TricksTableHex<T: AsRef<[Strain]>> {
deal: TricksTable,
seat: Seat,
strains: T,
}
impl<T: AsRef<[Strain]>> fmt::UpperHex for TricksTableHex<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for &strain in self.strains.as_ref() {
self.deal[strain].hex(self.seat).fmt(f)?;
}
Ok(())
}
}
impl TricksTable {
#[must_use]
pub fn hex(self, seat: Seat, strains: impl AsRef<[Strain]>) -> impl fmt::UpperHex {
TricksTableHex {
deal: self,
seat,
strains,
}
}
}
impl Strain {
#[must_use]
const fn to_sys(self) -> usize {
match self {
Self::Spades => 0,
Self::Hearts => 1,
Self::Diamonds => 2,
Self::Clubs => 3,
Self::Notrump => 4,
}
}
}
impl From<sys::ddTableResults> for TricksTable {
fn from(table: sys::ddTableResults) -> Self {
const fn make_row(row: [c_int; 4]) -> TricksRow {
#[allow(clippy::cast_sign_loss)]
TricksRow::new(
(row[0] & 0xFF) as u8,
(row[1] & 0xFF) as u8,
(row[2] & 0xFF) as u8,
(row[3] & 0xFF) as u8,
)
}
Self([
make_row(table.resTable[Strain::Clubs.to_sys()]),
make_row(table.resTable[Strain::Diamonds.to_sys()]),
make_row(table.resTable[Strain::Hearts.to_sys()]),
make_row(table.resTable[Strain::Spades.to_sys()]),
make_row(table.resTable[Strain::Notrump.to_sys()]),
])
}
}
impl From<TricksTable> for sys::ddTableResults {
fn from(table: TricksTable) -> Self {
const fn make_row(row: TricksRow) -> [c_int; 4] {
[
row.get(Seat::North) as c_int,
row.get(Seat::East) as c_int,
row.get(Seat::South) as c_int,
row.get(Seat::West) as c_int,
]
}
Self {
resTable: [
make_row(table[Strain::Spades]),
make_row(table[Strain::Hearts]),
make_row(table[Strain::Diamonds]),
make_row(table[Strain::Clubs]),
make_row(table[Strain::Notrump]),
],
}
}
}
impl From<Deal> for sys::ddTableDeal {
fn from(deal: Deal) -> Self {
Self {
cards: deal.0.map(|hand| {
[
hand[crate::Suit::Spades].to_bits().into(),
hand[crate::Suit::Hearts].to_bits().into(),
hand[crate::Suit::Diamonds].to_bits().into(),
hand[crate::Suit::Clubs].to_bits().into(),
]
}),
}
}
}
pub fn solve_deal(deal: Deal) -> Result<TricksTable, Error> {
let mut result = sys::ddTableResults::default();
let _guard = THREAD_POOL.lock()?;
let status = unsafe { sys::CalcDDtable(deal.into(), &mut result) };
Ok(SystemError::propagate(result.into(), status)?)
}
pub unsafe fn solve_deal_segment(
deals: &[Deal],
flags: StrainFlags,
) -> Result<sys::ddTablesRes, Error> {
debug_assert!(deals.len() * flags.bits().count_ones() as usize <= sys::MAXNOOFBOARDS as usize);
let mut pack = sys::ddTableDeals {
noOfTables: c_int::try_from(deals.len()).unwrap_or(c_int::MAX),
..Default::default()
};
deals
.iter()
.enumerate()
.for_each(|(i, &deal)| pack.deals[i] = deal.into());
let mut res = sys::ddTablesRes::default();
let _guard = THREAD_POOL.lock()?;
let status = sys::CalcAllTables(
&mut pack,
-1,
&mut [
c_int::from(!flags.contains(StrainFlags::SPADES)),
c_int::from(!flags.contains(StrainFlags::HEARTS)),
c_int::from(!flags.contains(StrainFlags::DIAMONDS)),
c_int::from(!flags.contains(StrainFlags::CLUBS)),
c_int::from(!flags.contains(StrainFlags::NOTRUMP)),
][0],
&mut res,
&mut sys::allParResults::default(),
);
Ok(SystemError::propagate(res, status)?)
}
pub fn solve_deals(deals: &[Deal], flags: StrainFlags) -> Result<Vec<TricksTable>, Error> {
let mut tables = Vec::new();
for chunk in deals.chunks((sys::MAXNOOFBOARDS / flags.bits().count_ones()) as usize) {
tables.extend(
unsafe { solve_deal_segment(chunk, flags) }?.results[..chunk.len()]
.iter()
.map(|&x| TricksTable::from(x)),
);
}
Ok(tables)
}
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Vulnerability: u8 {
const NS = 1;
const EW = 2;
}
}
impl Vulnerability {
#[must_use]
pub const fn to_sys(self) -> i32 {
const ALL: u8 = Vulnerability::all().bits();
const NS: u8 = Vulnerability::NS.bits();
const EW: u8 = Vulnerability::EW.bits();
match self.bits() {
0 => 0,
ALL => 1,
NS => 2,
EW => 3,
_ => unreachable!(),
}
}
}
#[derive(Debug, Clone)]
pub struct Par {
pub score: i32,
pub contracts: Vec<(crate::Contract, Seat, i8)>,
}
impl PartialEq for Par {
fn eq(&self, other: &Self) -> bool {
fn key(contracts: &[(crate::Contract, Seat, i8)]) -> u32 {
contracts
.iter()
.map(|&(contract, seat, _)| 1 << ((contract.bid.strain as u8) << 2 | seat as u8))
.fold(0, u32::bitor)
}
self.score == other.score && key(&self.contracts) == key(&other.contracts)
}
}
impl Eq for Par {}
impl From<sys::parResultsMaster> for Par {
fn from(par: sys::parResultsMaster) -> Self {
#[allow(clippy::cast_sign_loss)]
let len = par.number as usize * usize::from(par.contracts[0].level != 0);
#[allow(clippy::cast_sign_loss)]
let contracts = par.contracts[..len]
.iter()
.flat_map(|contract| {
let strain = [
Strain::Notrump,
Strain::Spades,
Strain::Hearts,
Strain::Diamonds,
Strain::Clubs,
][contract.denom as usize];
let (penalty, overtricks) = if contract.underTricks > 0 {
assert!(contract.underTricks <= 13);
(
crate::Penalty::Doubled,
-((contract.underTricks & 0xFF) as i8),
)
} else {
assert!(contract.overTricks >= 0 && contract.overTricks <= 13);
(crate::Penalty::None, (contract.overTricks & 0xFF) as i8)
};
assert_eq!(contract.level, contract.level & 7);
let seat: Seat = unsafe { core::mem::transmute((contract.seats & 3) as u8) };
let is_pair = contract.seats >= 4;
let contract = crate::Contract::new((contract.level & 7) as u8, strain, penalty);
core::iter::once((contract, seat, overtricks)).chain(if is_pair {
Some((contract, seat + core::num::Wrapping(2), overtricks))
} else {
None
})
})
.collect();
Self {
score: par.score,
contracts,
}
}
}
pub fn calculate_par(
tricks: TricksTable,
vul: Vulnerability,
dealer: Seat,
) -> Result<Par, SystemError> {
let mut par = sys::parResultsMaster::default();
let status = unsafe { sys::DealerParBin(&mut tricks.into(), &mut par, vul.to_sys(), dealer as c_int) };
Ok(SystemError::propagate(par, status)?.into())
}
pub fn calculate_pars(tricks: TricksTable, vul: Vulnerability) -> Result<[Par; 2], SystemError> {
let mut pars = [sys::parResultsMaster::default(); 2];
let status = unsafe { sys::SidesParBin(&mut tricks.into(), &mut pars[0], vul.to_sys()) };
Ok(SystemError::propagate(pars, status)?.map(Into::into))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Target {
Any(i8),
All(i8),
Legal,
}
impl Target {
#[must_use]
pub const fn target(self) -> c_int {
match self {
Self::Any(target) | Self::All(target) => target as c_int,
Self::Legal => -1,
}
}
#[must_use]
pub const fn solutions(self) -> c_int {
match self {
Self::Any(_) => 1,
Self::All(_) => 2,
Self::Legal => 3,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Board {
pub trump: Strain,
pub lead: Seat,
pub current_cards: [Option<crate::Card>; 3],
pub deal: Deal,
}
impl From<Board> for sys::deal {
fn from(board: Board) -> Self {
let mut suits = [0; 3];
let mut ranks = [0; 3];
for (i, card) in board.current_cards.into_iter().flatten().enumerate() {
suits[i] = 3 - card.suit() as c_int;
ranks[i] = c_int::from(card.rank());
}
Self {
trump: match board.trump {
Strain::Spades => 0,
Strain::Hearts => 1,
Strain::Diamonds => 2,
Strain::Clubs => 3,
Strain::Notrump => 4,
},
first: board.lead as c_int,
currentTrickSuit: suits,
currentTrickRank: ranks,
remainCards: sys::ddTableDeal::from(board.deal).cards,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Play {
pub card: crate::Card,
pub equals: crate::Holding,
pub score: i8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FoundPlays {
pub plays: [Option<Play>; 13],
pub nodes: u32,
}
impl From<sys::futureTricks> for FoundPlays {
#[allow(clippy::cast_sign_loss)]
fn from(future: sys::futureTricks) -> Self {
let mut plays = [None; 13];
plays
.iter_mut()
.enumerate()
.take(future.cards as usize)
.for_each(|(i, play)| {
let card = crate::Card::new(
crate::Suit::DESC[future.suit[i] as usize],
(future.rank[i] & 0xFF) as u8,
);
let equals = crate::Holding::from_bits((future.equals[i] & 0xFFFF) as u16);
let score = (future.score[i] & 0xFF) as i8;
*play = Some(Play {
card,
equals,
score,
});
});
Self {
plays,
nodes: future.nodes as u32,
}
}
}
pub fn solve_board(board: Board, target: Target) -> Result<FoundPlays, Error> {
let mut result = sys::futureTricks::default();
let status = unsafe {
let _guard = THREAD_POOL.lock()?;
sys::SolveBoard(
board.into(),
target.target(),
target.solutions(),
0,
&mut result,
0,
)
};
Ok(SystemError::propagate(result, status)?.into())
}
pub unsafe fn solve_board_segment(args: &[(Board, Target)]) -> Result<sys::solvedBoards, Error> {
debug_assert!(args.len() <= sys::MAXNOOFBOARDS as usize);
let mut pack = sys::boards {
noOfBoards: c_int::try_from(args.len()).unwrap_or(c_int::MAX),
..Default::default()
};
args.iter().enumerate().for_each(|(i, &(board, target))| {
pack.deals[i] = board.into();
pack.target[i] = target.target();
pack.solutions[i] = target.solutions();
});
let mut res = sys::solvedBoards::default();
let _guard = THREAD_POOL.lock()?;
let status = unsafe { sys::SolveAllBoardsBin(&mut pack, &mut res) };
Ok(SystemError::propagate(res, status)?)
}
pub fn solve_boards(args: &[(Board, Target)]) -> Result<Vec<FoundPlays>, Error> {
let mut solutions = Vec::new();
for chunk in args.chunks(sys::MAXNOOFBOARDS as usize) {
solutions.extend(
unsafe { solve_board_segment(chunk) }?.solvedBoard[..chunk.len()]
.iter()
.map(|&x| FoundPlays::from(x)),
);
}
Ok(solutions)
}