Skip to main content

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