dds_bridge/solver/
mod.rs

1#[cfg(test)]
2mod test;
3
4use crate::contract::{Contract, Penalty, Strain};
5use crate::deal::{Card, Deal, Holding, Seat, Suit};
6use core::ffi::c_int;
7use core::fmt;
8use core::num::Wrapping;
9use core::ops::BitOr as _;
10use dds_bridge_sys as sys;
11use once_cell::sync::Lazy;
12use std::sync::{Mutex, PoisonError};
13use thiserror::Error;
14
15static THREAD_POOL: Lazy<Mutex<()>> = Lazy::new(|| {
16    // SAFETY: just initializing the thread pool
17    unsafe { sys::SetMaxThreads(0) };
18    Mutex::new(())
19});
20
21/// Errors that occurred in [`dds_bridge_sys`]
22#[derive(Debug, Error, Clone, Copy, PartialEq, Eq, Hash)]
23#[repr(i32)]
24pub enum SystemError {
25    /// Success, no error
26    #[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_NO_FAULT) })]
27    #[allow(clippy::cast_possible_wrap)]
28    Success = sys::RETURN_NO_FAULT as i32,
29
30    /// A general or unknown error
31    #[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_UNKNOWN_FAULT) })]
32    UnknownFault = sys::RETURN_UNKNOWN_FAULT,
33
34    /// Zero cards
35    #[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_ZERO_CARDS) })]
36    ZeroCards = sys::RETURN_ZERO_CARDS,
37
38    /// Target exceeds number of tricks
39    #[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_TARGET_TOO_HIGH) })]
40    TargetTooHigh = sys::RETURN_TARGET_TOO_HIGH,
41
42    /// Duplicate cards
43    #[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_DUPLICATE_CARDS) })]
44    DuplicateCards = sys::RETURN_DUPLICATE_CARDS,
45
46    /// Target tricks < 0
47    #[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_TARGET_WRONG_LO) })]
48    NegativeTarget = sys::RETURN_TARGET_WRONG_LO,
49
50    /// Target tricks > 13
51    #[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_TARGET_WRONG_HI) })]
52    InvalidTarget = sys::RETURN_TARGET_WRONG_HI,
53
54    /// Solving parameter < 1
55    #[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_SOLNS_WRONG_LO) })]
56    LowSolvingParameter = sys::RETURN_SOLNS_WRONG_LO,
57
58    /// Solving parameter > 3
59    #[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_SOLNS_WRONG_HI) })]
60    HighSolvingParameter = sys::RETURN_SOLNS_WRONG_HI,
61
62    /// Too many cards
63    #[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_TOO_MANY_CARDS) })]
64    TooManyCards = sys::RETURN_TOO_MANY_CARDS,
65
66    /// Wrong current suit or rank
67    #[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_SUIT_OR_RANK) })]
68    CurrentSuitOrRank = sys::RETURN_SUIT_OR_RANK,
69
70    /// Wrong played card
71    #[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_PLAYED_CARD) })]
72    PlayedCard = sys::RETURN_PLAYED_CARD,
73
74    /// Wrong card count
75    #[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_CARD_COUNT) })]
76    CardCount = sys::RETURN_CARD_COUNT,
77
78    /// Wrong thread index
79    #[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_THREAD_INDEX) })]
80    ThreadIndex = sys::RETURN_THREAD_INDEX,
81
82    /// Mode parameter < 0
83    #[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_MODE_WRONG_LO) })]
84    NegativeModeParameter = sys::RETURN_MODE_WRONG_LO,
85
86    /// Mode parameter > 2
87    #[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_MODE_WRONG_HI) })]
88    HighModeParameter = sys::RETURN_MODE_WRONG_HI,
89
90    /// Wrong trump suit
91    #[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_TRUMP_WRONG) })]
92    Trump = sys::RETURN_TRUMP_WRONG,
93
94    /// Wrong "first"
95    #[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_FIRST_WRONG) })]
96    First = sys::RETURN_FIRST_WRONG,
97
98    /// `AnalysePlay*()` family of functions.
99    /// (a) Less than 0 or more than 52 cards supplied.
100    /// (b) Invalid suit or rank supplied.
101    /// (c) A played card is not held by the right player.
102    #[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_PLAY_FAULT) })]
103    AnalysePlay = sys::RETURN_PLAY_FAULT,
104
105    /// Invalid PBN
106    #[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_PBN_FAULT) })]
107    PBN = sys::RETURN_PBN_FAULT,
108
109    /// Too many boards
110    #[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_TOO_MANY_BOARDS) })]
111    TooManyBoards = sys::RETURN_TOO_MANY_BOARDS,
112
113    /// Cannot create a new thread
114    #[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_THREAD_CREATE) })]
115    ThreadCreate = sys::RETURN_THREAD_CREATE,
116
117    /// Failed to wait for a thread
118    #[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_THREAD_WAIT) })]
119    ThreadWait = sys::RETURN_THREAD_WAIT,
120
121    /// Missing threading system
122    #[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_THREAD_MISSING) })]
123    ThreadMissing = sys::RETURN_THREAD_MISSING,
124
125    /// No suit to solve
126    #[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_NO_SUIT) })]
127    NoSuit = sys::RETURN_NO_SUIT,
128
129    /// Too many tables
130    #[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_TOO_MANY_TABLES) })]
131    TooManyTables = sys::RETURN_TOO_MANY_TABLES,
132
133    /// Invalid chunk size
134    #[error("{}", unsafe { core::str::from_utf8_unchecked(sys::TEXT_CHUNK_SIZE) })]
135    ChunkSize = sys::RETURN_CHUNK_SIZE,
136}
137
138impl SystemError {
139    /// Propagate a status code to an error
140    ///
141    /// - `x`: Arbitrary data to return if `status` is non-negative (success)
142    /// - `status`: The status code from a DDS function
143    ///
144    /// # Errors
145    /// A [`SystemError`] specified by `status`
146    pub const fn propagate<T: Copy>(x: T, status: i32) -> Result<T, Self> {
147        match status {
148            0.. => Ok(x),
149            sys::RETURN_ZERO_CARDS => Err(Self::ZeroCards),
150            sys::RETURN_TARGET_TOO_HIGH => Err(Self::TargetTooHigh),
151            sys::RETURN_DUPLICATE_CARDS => Err(Self::DuplicateCards),
152            sys::RETURN_TARGET_WRONG_LO => Err(Self::NegativeTarget),
153            sys::RETURN_TARGET_WRONG_HI => Err(Self::InvalidTarget),
154            sys::RETURN_SOLNS_WRONG_LO => Err(Self::LowSolvingParameter),
155            sys::RETURN_SOLNS_WRONG_HI => Err(Self::HighSolvingParameter),
156            sys::RETURN_TOO_MANY_CARDS => Err(Self::TooManyCards),
157            sys::RETURN_SUIT_OR_RANK => Err(Self::CurrentSuitOrRank),
158            sys::RETURN_PLAYED_CARD => Err(Self::PlayedCard),
159            sys::RETURN_CARD_COUNT => Err(Self::CardCount),
160            sys::RETURN_THREAD_INDEX => Err(Self::ThreadIndex),
161            sys::RETURN_MODE_WRONG_LO => Err(Self::NegativeModeParameter),
162            sys::RETURN_MODE_WRONG_HI => Err(Self::HighModeParameter),
163            sys::RETURN_TRUMP_WRONG => Err(Self::Trump),
164            sys::RETURN_FIRST_WRONG => Err(Self::First),
165            sys::RETURN_PLAY_FAULT => Err(Self::AnalysePlay),
166            sys::RETURN_PBN_FAULT => Err(Self::PBN),
167            sys::RETURN_TOO_MANY_BOARDS => Err(Self::TooManyBoards),
168            sys::RETURN_THREAD_CREATE => Err(Self::ThreadCreate),
169            sys::RETURN_THREAD_WAIT => Err(Self::ThreadWait),
170            sys::RETURN_THREAD_MISSING => Err(Self::ThreadMissing),
171            sys::RETURN_NO_SUIT => Err(Self::NoSuit),
172            sys::RETURN_TOO_MANY_TABLES => Err(Self::TooManyTables),
173            sys::RETURN_CHUNK_SIZE => Err(Self::ChunkSize),
174            _ => Err(Self::UnknownFault),
175        }
176    }
177}
178
179/// The sum type of all solver errors
180#[derive(Debug, Error)]
181pub enum Error {
182    /// An error propagated from [`dds_bridge_sys`]
183    #[error(transparent)]
184    System(SystemError),
185
186    /// A poisoned mutex of the thread pool
187    #[error("The thread pool is poisoned")]
188    Poison,
189}
190
191impl From<SystemError> for Error {
192    fn from(err: SystemError) -> Self {
193        Self::System(err)
194    }
195}
196
197impl<T> From<PoisonError<T>> for Error {
198    fn from(_: PoisonError<T>) -> Self {
199        Self::Poison
200    }
201}
202
203bitflags::bitflags! {
204    /// Flags for the solver to solve for a strain
205    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
206    pub struct StrainFlags : u8 {
207        /// Solve for clubs ([`Strain::Clubs`])
208        const CLUBS = 0x01;
209        /// Solve for diamonds ([`Strain::Diamonds`])
210        const DIAMONDS = 0x02;
211        /// Solve for hearts ([`Strain::Hearts`])
212        const HEARTS = 0x04;
213        /// Solve for spades ([`Strain::Spades`])
214        const SPADES = 0x08;
215        /// Solve for notrump ([`Strain::Notrump`])
216        const NOTRUMP = 0x10;
217    }
218}
219
220/// Tricks that each seat can take as declarer for a strain
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
222pub struct TricksRow(u16);
223
224impl TricksRow {
225    /// Create a new row from the number of tricks each seat can take
226    #[must_use]
227    pub const fn new(n: u8, e: u8, s: u8, w: u8) -> Self {
228        Self(
229            (n as u16) << (4 * Seat::North as u8)
230                | (e as u16) << (4 * Seat::East as u8)
231                | (s as u16) << (4 * Seat::South as u8)
232                | (w as u16) << (4 * Seat::West as u8),
233        )
234    }
235
236    /// Get the number of tricks a seat can take as declarer
237    #[must_use]
238    pub const fn get(self, seat: Seat) -> u8 {
239        (self.0 >> (4 * seat as u8) & 0xF) as u8
240    }
241
242    /// Hexadecimal representation from a seat's perspective
243    #[must_use]
244    pub fn hex(self, seat: Seat) -> impl fmt::UpperHex {
245        TricksRowHex { deal: self, seat }
246    }
247}
248
249struct TricksRowHex {
250    deal: TricksRow,
251    seat: Seat,
252}
253
254impl fmt::UpperHex for TricksRowHex {
255    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
256        write!(
257            f,
258            "{:X}{:X}{:X}{:X}",
259            self.deal.get(self.seat),
260            self.deal.get(self.seat + Wrapping(1)),
261            self.deal.get(self.seat + Wrapping(2)),
262            self.deal.get(self.seat + Wrapping(3)),
263        )
264    }
265}
266
267/// Tricks that each seat can take as declarer for all strains
268#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
269pub struct TricksTable(pub [TricksRow; 5]);
270
271impl core::ops::Index<Strain> for TricksTable {
272    type Output = TricksRow;
273
274    fn index(&self, strain: Strain) -> &TricksRow {
275        &self.0[strain as usize]
276    }
277}
278
279struct TricksTableHex<T: AsRef<[Strain]>> {
280    deal: TricksTable,
281    seat: Seat,
282    strains: T,
283}
284
285impl<T: AsRef<[Strain]>> fmt::UpperHex for TricksTableHex<T> {
286    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
287        for &strain in self.strains.as_ref() {
288            self.deal[strain].hex(self.seat).fmt(f)?;
289        }
290        Ok(())
291    }
292}
293
294impl TricksTable {
295    /// Hexadecimal representation from a seat's perspective
296    #[must_use]
297    pub fn hex(self, seat: Seat, strains: impl AsRef<[Strain]>) -> impl fmt::UpperHex {
298        TricksTableHex {
299            deal: self,
300            seat,
301            strains,
302        }
303    }
304}
305
306impl Strain {
307    /// Convert to the index in [`dds_bridge_sys`]
308    #[must_use]
309    const fn to_sys(self) -> usize {
310        match self {
311            Self::Spades => 0,
312            Self::Hearts => 1,
313            Self::Diamonds => 2,
314            Self::Clubs => 3,
315            Self::Notrump => 4,
316        }
317    }
318}
319
320impl From<sys::ddTableResults> for TricksTable {
321    fn from(table: sys::ddTableResults) -> Self {
322        const fn make_row(row: [c_int; 4]) -> TricksRow {
323            #[allow(clippy::cast_sign_loss)]
324            TricksRow::new(
325                (row[0] & 0xFF) as u8,
326                (row[1] & 0xFF) as u8,
327                (row[2] & 0xFF) as u8,
328                (row[3] & 0xFF) as u8,
329            )
330        }
331
332        Self([
333            make_row(table.resTable[Strain::Clubs.to_sys()]),
334            make_row(table.resTable[Strain::Diamonds.to_sys()]),
335            make_row(table.resTable[Strain::Hearts.to_sys()]),
336            make_row(table.resTable[Strain::Spades.to_sys()]),
337            make_row(table.resTable[Strain::Notrump.to_sys()]),
338        ])
339    }
340}
341
342impl From<TricksTable> for sys::ddTableResults {
343    fn from(table: TricksTable) -> Self {
344        const fn make_row(row: TricksRow) -> [c_int; 4] {
345            [
346                row.get(Seat::North) as c_int,
347                row.get(Seat::East) as c_int,
348                row.get(Seat::South) as c_int,
349                row.get(Seat::West) as c_int,
350            ]
351        }
352
353        Self {
354            resTable: [
355                make_row(table[Strain::Spades]),
356                make_row(table[Strain::Hearts]),
357                make_row(table[Strain::Diamonds]),
358                make_row(table[Strain::Clubs]),
359                make_row(table[Strain::Notrump]),
360            ],
361        }
362    }
363}
364
365impl From<Deal> for sys::ddTableDeal {
366    fn from(deal: Deal) -> Self {
367        Self {
368            cards: deal.0.map(|hand| {
369                [
370                    hand[Suit::Spades].to_bits().into(),
371                    hand[Suit::Hearts].to_bits().into(),
372                    hand[Suit::Diamonds].to_bits().into(),
373                    hand[Suit::Clubs].to_bits().into(),
374                ]
375            }),
376        }
377    }
378}
379
380/// Solve a single deal with [`sys::CalcDDtable`]
381///
382/// # Errors
383/// A [`SystemError`] propagated from DDS or a [`std::sync::PoisonError`]
384pub fn solve_deal(deal: Deal) -> Result<TricksTable, Error> {
385    let mut result = sys::ddTableResults::default();
386    let _guard = THREAD_POOL.lock()?;
387    // SAFETY: `_guard` just locked the thread pool
388    let status = unsafe { sys::CalcDDtable(deal.into(), &mut result) };
389    Ok(SystemError::propagate(result.into(), status)?)
390}
391
392/// Solve deals with a single call of [`sys::CalcAllTables`]
393///
394/// - `deals`: A slice of deals to solve
395/// - `flags`: Flags of strains to solve for
396///
397/// # Safety
398/// `deals.len() * flags.bits().count_ones()` must not exceed
399/// [`sys::MAXNOOFBOARDS`].
400///
401/// # Errors
402/// A [`SystemError`] propagated from DDS or a [`std::sync::PoisonError`]
403pub unsafe fn solve_deal_segment(
404    deals: &[Deal],
405    flags: StrainFlags,
406) -> Result<sys::ddTablesRes, Error> {
407    debug_assert!(deals.len() * flags.bits().count_ones() as usize <= sys::MAXNOOFBOARDS as usize);
408    let mut pack = sys::ddTableDeals {
409        noOfTables: c_int::try_from(deals.len()).unwrap_or(c_int::MAX),
410        ..Default::default()
411    };
412    deals
413        .iter()
414        .enumerate()
415        .for_each(|(i, &deal)| pack.deals[i] = deal.into());
416
417    let mut res = sys::ddTablesRes::default();
418    let _guard = THREAD_POOL.lock()?;
419    let status = sys::CalcAllTables(
420        &mut pack,
421        -1,
422        &mut [
423            c_int::from(!flags.contains(StrainFlags::SPADES)),
424            c_int::from(!flags.contains(StrainFlags::HEARTS)),
425            c_int::from(!flags.contains(StrainFlags::DIAMONDS)),
426            c_int::from(!flags.contains(StrainFlags::CLUBS)),
427            c_int::from(!flags.contains(StrainFlags::NOTRUMP)),
428        ][0],
429        &mut res,
430        &mut sys::allParResults::default(),
431    );
432    Ok(SystemError::propagate(res, status)?)
433}
434
435/// Solve deals in parallel for given strains
436///
437/// - `deals`: A slice of deals to solve
438/// - `flags`: Flags of strains to solve for
439///
440/// # Errors
441/// A [`SystemError`] propagated from DDS or a [`std::sync::PoisonError`]
442pub fn solve_deals(deals: &[Deal], flags: StrainFlags) -> Result<Vec<TricksTable>, Error> {
443    let mut tables = Vec::new();
444    for chunk in deals.chunks((sys::MAXNOOFBOARDS / flags.bits().count_ones()) as usize) {
445        tables.extend(
446            // SAFETY: the thread pool is locked inside `solve_deal_segment`
447            unsafe { solve_deal_segment(chunk, flags) }?.results[..chunk.len()]
448                .iter()
449                .map(|&x| TricksTable::from(x)),
450        );
451    }
452    Ok(tables)
453}
454
455bitflags::bitflags! {
456    /// Vulnerability of pairs
457    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
458    pub struct Vulnerability: u8 {
459        /// North-South are vulnerable
460        const NS = 1;
461        /// East-West are vulnerable
462        const EW = 2;
463    }
464}
465
466impl Vulnerability {
467    /// Convert to encoding in [`dds_bridge_sys`]
468    #[must_use]
469    #[inline]
470    pub const fn to_sys(self) -> i32 {
471        const ALL: u8 = Vulnerability::all().bits();
472        const NS: u8 = Vulnerability::NS.bits();
473        const EW: u8 = Vulnerability::EW.bits();
474
475        match self.bits() {
476            0 => 0,
477            ALL => 1,
478            NS => 2,
479            EW => 3,
480            _ => unreachable!(),
481        }
482    }
483
484    /// Conditionally swap bits
485    ///
486    /// This method makes rotating the vulnerability pair easy.  This method is
487    /// `const` and `#[inline]` to maximize chances of constant folding
488    /// `.swap(true)`.
489    #[must_use]
490    #[inline]
491    pub const fn swap(self, condition: bool) -> Self {
492        Self::from_bits_truncate(self.bits() * (condition as u8 * 3 + 2) / 2)
493    }
494}
495
496/// Exhaustively check correctness of [`Vulnerability::swap`] at compile time
497const _: () = {
498    const ALL: Vulnerability = Vulnerability::all();
499    const NONE: Vulnerability = Vulnerability::empty();
500
501    assert!(matches!(ALL.swap(true), ALL));
502    assert!(matches!(NONE.swap(true), NONE));
503    assert!(matches!(Vulnerability::NS.swap(true), Vulnerability::EW));
504    assert!(matches!(Vulnerability::EW.swap(true), Vulnerability::NS));
505
506    assert!(matches!(ALL.swap(false), ALL));
507    assert!(matches!(NONE.swap(false), NONE));
508    assert!(matches!(Vulnerability::NS.swap(false), Vulnerability::NS));
509    assert!(matches!(Vulnerability::EW.swap(false), Vulnerability::EW));
510};
511
512/// Par score and contracts
513#[derive(Debug, Clone)]
514pub struct Par {
515    /// The par score
516    pub score: i32,
517
518    /// The contracts that achieve the par score
519    ///
520    /// Each tuple contains a contract, the declarer, and the number of
521    /// overtricks (undertricks in negative).
522    pub contracts: Vec<(Contract, Seat, i8)>,
523}
524
525impl PartialEq for Par {
526    fn eq(&self, other: &Self) -> bool {
527        // Since every contract scores the same, we can compare only the set of
528        // (`Strain`, `Seat`).  Also, #`Strain` * #`Seat` is 20, which fits in
529        // a `u32` as a bitset.
530        fn key(contracts: &[(Contract, Seat, i8)]) -> u32 {
531            contracts
532                .iter()
533                .map(|&(contract, seat, _)| 1 << ((contract.bid.strain as u8) << 2 | seat as u8))
534                .fold(0, u32::bitor)
535        }
536        self.score == other.score && key(&self.contracts) == key(&other.contracts)
537    }
538}
539
540impl Eq for Par {}
541
542impl From<sys::parResultsMaster> for Par {
543    fn from(par: sys::parResultsMaster) -> Self {
544        // DDS returns a zero contract for par-zero deals, but we want to filter
545        // it out for consistency.
546        #[allow(clippy::cast_sign_loss)]
547        let len = par.number as usize * usize::from(par.contracts[0].level != 0);
548
549        #[allow(clippy::cast_sign_loss)]
550        let contracts = par.contracts[..len]
551            .iter()
552            .flat_map(|contract| {
553                let strain = [
554                    Strain::Notrump,
555                    Strain::Spades,
556                    Strain::Hearts,
557                    Strain::Diamonds,
558                    Strain::Clubs,
559                ][contract.denom as usize];
560
561                let (penalty, overtricks) = if contract.underTricks > 0 {
562                    assert!(contract.underTricks <= 13);
563                    (Penalty::Doubled, -((contract.underTricks & 0xFF) as i8))
564                } else {
565                    assert!(contract.overTricks >= 0 && contract.overTricks <= 13);
566                    (Penalty::None, (contract.overTricks & 0xFF) as i8)
567                };
568
569                assert_eq!(contract.level, contract.level & 7);
570                // SAFETY: `contract.seats & 3` is in the range of 0..=3 and hence a valid `Seat`
571                let seat: Seat = unsafe { core::mem::transmute((contract.seats & 3) as u8) };
572                let is_pair = contract.seats >= 4;
573                let contract = Contract::new((contract.level & 7) as u8, strain, penalty);
574
575                core::iter::once((contract, seat, overtricks)).chain(if is_pair {
576                    Some((contract, seat + core::num::Wrapping(2), overtricks))
577                } else {
578                    None
579                })
580            })
581            .collect();
582
583        Self {
584            score: par.score,
585            contracts,
586        }
587    }
588}
589
590/// Calculate par score and contracts for a deal
591///
592/// - `tricks`: The number of tricks each seat can take as declarer for each strain
593/// - `vul`: The vulnerability of pairs
594/// - `dealer`: The dealer of the deal
595///
596/// # Errors
597/// A [`SystemError`] propagated from DDS
598pub fn calculate_par(
599    tricks: TricksTable,
600    vul: Vulnerability,
601    dealer: Seat,
602) -> Result<Par, SystemError> {
603    let mut par = sys::parResultsMaster::default();
604    let status = // SAFETY: calculating par is reentrant
605        unsafe { sys::DealerParBin(&mut tricks.into(), &mut par, vul.to_sys(), dealer as c_int) };
606    Ok(SystemError::propagate(par, status)?.into())
607}
608
609/// Calculate par scores for both pairs
610///
611/// - `tricks`: The number of tricks each seat can take as declarer for each strain
612/// - `vul`: The vulnerability of pairs
613///
614/// # Errors
615/// A [`SystemError`] propagated from DDS
616pub fn calculate_pars(tricks: TricksTable, vul: Vulnerability) -> Result<[Par; 2], SystemError> {
617    let mut pars = [sys::parResultsMaster::default(); 2];
618    // SAFE: calculating par is reentrant
619    let status = unsafe { sys::SidesParBin(&mut tricks.into(), &mut pars[0], vul.to_sys()) };
620    Ok(SystemError::propagate(pars, status)?.map(Into::into))
621}
622
623/// Target tricks and number of solutions to find
624///
625/// This enum corresponds to a tuple of `target` and `solutions` in
626/// [`sys::SolveBoard`].  The `target` tricks given as an associated value must
627/// be in the range of `-1..=13`, where `-1` instructs the solver to find cards
628/// that give the most tricks.
629#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
630pub enum Target {
631    /// Find any card that fulfills the target
632    ///
633    /// - `0..=13`: Find any card scoring at least `target` tricks
634    /// - `-1`: Find any card scoring the most tricks
635    Any(i8),
636
637    /// Find all cards that fulfill the target
638    ///
639    /// - `0..=13`: Find all cards scoring at least `target` tricks
640    /// - `-1`: Find all cards scoring the most tricks
641    All(i8),
642
643    /// Solve for all legal plays
644    ///
645    /// Cards are sorted with their scores in descending order.
646    Legal,
647}
648
649impl Target {
650    /// Get the `target` argument for [`sys::SolveBoard`]
651    #[must_use]
652    #[inline]
653    pub const fn target(self) -> c_int {
654        match self {
655            Self::Any(target) | Self::All(target) => target as c_int,
656            Self::Legal => -1,
657        }
658    }
659
660    /// Get the `solutions` argument for [`sys::SolveBoard`]
661    #[must_use]
662    #[inline]
663    pub const fn solutions(self) -> c_int {
664        match self {
665            Self::Any(_) => 1,
666            Self::All(_) => 2,
667            Self::Legal => 3,
668        }
669    }
670}
671
672/// A snapshot of a board
673#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
674pub struct Board {
675    /// The strain of the contract
676    pub trump: Strain,
677    /// The player leading the trick
678    pub lead: Seat,
679    /// The played cards in the current trick
680    pub current_cards: [Option<Card>; 3],
681    /// The remaining cards in the deal
682    pub deal: Deal,
683}
684
685impl From<Board> for sys::deal {
686    fn from(board: Board) -> Self {
687        let mut suits = [0; 3];
688        let mut ranks = [0; 3];
689
690        for (i, card) in board.current_cards.into_iter().flatten().enumerate() {
691            suits[i] = 3 - card.suit() as c_int;
692            ranks[i] = c_int::from(card.rank());
693        }
694
695        Self {
696            trump: match board.trump {
697                Strain::Spades => 0,
698                Strain::Hearts => 1,
699                Strain::Diamonds => 2,
700                Strain::Clubs => 3,
701                Strain::Notrump => 4,
702            },
703            first: board.lead as c_int,
704            currentTrickSuit: suits,
705            currentTrickRank: ranks,
706            remainCards: sys::ddTableDeal::from(board.deal).cards,
707        }
708    }
709}
710
711/// A play and its consequences
712#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
713pub struct Play {
714    /// The card to play, the highest in a sequence
715    ///
716    /// For example, if the solution is to play a card from ♥KQJ, this field
717    /// would be ♥K.
718    pub card: Card,
719
720    /// Lower equals in the sequence
721    ///
722    /// Playing any card in a sequence is equal in bridge and many trick-taking
723    /// games.  This field contains lower cards in the sequence as `card`.  For
724    /// example, if the solution is to play KQJ, this field would contain QJ.
725    pub equals: Holding,
726
727    /// Tricks this play would score
728    pub score: i8,
729}
730
731/// Solved plays for a board
732#[derive(Debug, Clone, Copy, PartialEq, Eq)]
733pub struct FoundPlays {
734    /// The plays and their consequences
735    pub plays: [Option<Play>; 13],
736    /// The number of nodes searched by the solver
737    pub nodes: u32,
738}
739
740impl From<sys::futureTricks> for FoundPlays {
741    #[allow(clippy::cast_sign_loss)]
742    fn from(future: sys::futureTricks) -> Self {
743        let mut plays = [None; 13];
744        plays
745            .iter_mut()
746            .enumerate()
747            .take(future.cards as usize)
748            .for_each(|(i, play)| {
749                let card = Card::new(
750                    Suit::DESC[future.suit[i] as usize],
751                    (future.rank[i] & 0xFF) as u8,
752                );
753                let equals = Holding::from_bits_truncate((future.equals[i] & 0xFFFF) as u16);
754                let score = (future.score[i] & 0xFF) as i8;
755                *play = Some(Play {
756                    card,
757                    equals,
758                    score,
759                });
760            });
761        Self {
762            plays,
763            nodes: future.nodes as u32,
764        }
765    }
766}
767
768/// Solve a single board with [`sys::SolveBoard`]
769///
770/// - `board`: The board to solve
771/// - `target`: The target tricks and number of solutions to find
772///
773/// # Errors
774/// A [`SystemError`] propagated from DDS or a [`std::sync::PoisonError`]
775pub fn solve_board(board: Board, target: Target) -> Result<FoundPlays, Error> {
776    let mut result = sys::futureTricks::default();
777    // SAFETY: `_guard` locks the thread pool
778    let status = unsafe {
779        let _guard = THREAD_POOL.lock()?;
780        sys::SolveBoard(
781            board.into(),
782            target.target(),
783            target.solutions(),
784            0,
785            &mut result,
786            //TODO: Enable multithreading
787            0,
788        )
789    };
790    Ok(SystemError::propagate(result, status)?.into())
791}
792
793/// Solve boards with a single call of [`sys::SolveAllBoardsBin`]
794///
795/// - `args`: A slice of boards and their targets to solve
796///
797/// # Safety
798/// `args.len()` must not exceed [`sys::MAXNOOFBOARDS`].
799///
800/// # Errors
801/// A [`SystemError`] propagated from DDS or a [`std::sync::PoisonError`]
802pub unsafe fn solve_board_segment(args: &[(Board, Target)]) -> Result<sys::solvedBoards, Error> {
803    debug_assert!(args.len() <= sys::MAXNOOFBOARDS as usize);
804    let mut pack = sys::boards {
805        noOfBoards: c_int::try_from(args.len()).unwrap_or(c_int::MAX),
806        ..Default::default()
807    };
808    args.iter().enumerate().for_each(|(i, &(board, target))| {
809        pack.deals[i] = board.into();
810        pack.target[i] = target.target();
811        pack.solutions[i] = target.solutions();
812    });
813    let mut res = sys::solvedBoards::default();
814    let _guard = THREAD_POOL.lock()?;
815    // SAFETY: `_guard` just locked the thread pool
816    let status = unsafe { sys::SolveAllBoardsBin(&mut pack, &mut res) };
817    Ok(SystemError::propagate(res, status)?)
818}
819
820/// Solve boards in parallel
821///
822/// - `args`: A slice of boards and their targets to solve
823///
824/// # Errors
825/// A [`SystemError`] propagated from DDS or a [`std::sync::PoisonError`]
826pub fn solve_boards(args: &[(Board, Target)]) -> Result<Vec<FoundPlays>, Error> {
827    let mut solutions = Vec::new();
828    for chunk in args.chunks(sys::MAXNOOFBOARDS as usize) {
829        solutions.extend(
830            // SAFETY: the thread pool is locked inside `solve_board_segment`
831            unsafe { solve_board_segment(chunk) }?.solvedBoard[..chunk.len()]
832                .iter()
833                .map(|&x| FoundPlays::from(x)),
834        );
835    }
836    Ok(solutions)
837}