#[cfg(test)]
mod test;
use crate::contract::Strain;
use crate::deal::{Card, Deal, Hand, Holding, Seat, Suit};
use bitflags::bitflags;
use core::ffi::c_int;
use core::fmt;
use dds_bridge_sys as sys;
use once_cell::sync::Lazy;
use std::sync::{Mutex, MutexGuard, PoisonError};
use thiserror::Error;
static THREAD_POOL: Lazy<Mutex<()>> = Lazy::new(|| {
unsafe { sys::SetMaxThreads(0) };
Mutex::new(())
});
#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
#[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)]
pub enum Error {
System(SystemError),
Lock(PoisonError<MutexGuard<'static, ()>>),
}
impl From<SystemError> for Error {
fn from(err: SystemError) -> Self {
Self::System(err)
}
}
impl From<PoisonError<MutexGuard<'static, ()>>> for Error {
fn from(err: PoisonError<MutexGuard<'static, ()>>) -> Self {
Self::Lock(err)
}
}
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)]
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 at(self, seat: Seat) -> u8 {
(self.0 >> (4 * seat as u8) & 0xF) as u8
}
}
impl fmt::Display for TricksRow {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{:X}{:X}{:X}{:X}",
self.at(Seat::North),
self.at(Seat::East),
self.at(Seat::South),
self.at(Seat::West)
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TricksTable([TricksRow; 5]);
impl core::ops::Index<Strain> for TricksTable {
type Output = TricksRow;
fn index(&self, strain: Strain) -> &TricksRow {
&self.0[strain as usize]
}
}
impl fmt::Display for TricksTable {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}{}{}{}{}",
self.0[0], self.0[1], self.0[2], self.0[3], self.0[4]
)
}
}
const fn make_row(row: [c_int; 4]) -> TricksRow {
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
TricksRow::new(row[0] as u8, row[1] as u8, row[2] as u8, row[3] as u8)
}
impl From<sys::ddTableResults> for TricksTable {
fn from(table: sys::ddTableResults) -> Self {
const S: usize = 0;
const H: usize = 1;
const D: usize = 2;
const C: usize = 3;
const NT: usize = 4;
Self([
make_row(table.resTable[C]),
make_row(table.resTable[D]),
make_row(table.resTable[H]),
make_row(table.resTable[S]),
make_row(table.resTable[NT]),
])
}
}
impl From<Deal> for sys::ddTableDeal {
fn from(deal: Deal) -> Self {
fn convert(hand: Hand) -> [core::ffi::c_uint; 4] {
[
hand[Suit::Spades].to_bits().into(),
hand[Suit::Hearts].to_bits().into(),
hand[Suit::Diamonds].to_bits().into(),
hand[Suit::Clubs].to_bits().into(),
]
}
Self {
cards: [
convert(deal[Seat::North]),
convert(deal[Seat::East]),
convert(deal[Seat::South]),
convert(deal[Seat::West]),
],
}
}
}
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 {
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
noOfTables: deals.len() as c_int,
..Default::default()
};
deals
.iter()
.copied()
.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()
.copied()
.map(TricksTable::from),
);
}
Ok(tables)
}
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, Copy, PartialEq, Eq)]
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, PartialEq, Eq)]
pub struct Board {
pub trump: Strain,
pub lead: Seat,
pub current_cards: arrayvec::ArrayVec<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.iter().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)]
pub struct Play {
pub card: Card,
pub equals: Holding,
pub score: i8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FoundPlays {
pub plays: arrayvec::ArrayVec<Play, 13>,
pub nodes: u32,
}
impl From<sys::futureTricks> for FoundPlays {
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
fn from(future: sys::futureTricks) -> Self {
let mut plays = arrayvec::ArrayVec::new();
for i in 0..future.cards as usize {
let card = Card {
suit: Suit::DESCENDING[future.suit[i] as usize],
rank: future.rank[i] as u8,
};
let equals = Holding::from_bits(future.equals[i] as u16);
let score = future.score[i] as i8;
plays.push(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 {
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
noOfBoards: args.len() as c_int,
..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()
.copied()
.map(FoundPlays::from),
);
}
Ok(solutions)
}