Skip to main content

dds_bridge/
solver.rs

1//! Double-dummy solver and par-calculation bindings built on [`dds_bridge_sys`].
2//!
3//! # Panic policy
4//!
5//! The solver entry points in this module — [`calculate_par`],
6//! [`calculate_pars`], and the [`Solver`] methods
7//! [`solve_deal`](Solver::solve_deal), [`solve_deals`](Solver::solve_deals),
8//! [`solve_board`](Solver::solve_board), [`solve_boards`](Solver::solve_boards),
9//! [`analyse_play`](Solver::analyse_play), and
10//! [`analyse_plays`](Solver::analyse_plays) — are not expected to panic.
11//! They map DDS status codes through an internal helper that panics on error,
12//! but reaching that panic means either invalid input slipped past a safe
13//! constructor or DDS itself misbehaved. Either case is a bug — please report
14//! it.
15//!
16//! This policy does not cover validator panics from safe constructors
17//! (e.g. [`TrickCountRow::new`](crate::solver::TrickCountRow::new)), which
18//! panic by design on out-of-range inputs and have `try_*` counterparts for
19//! fallible construction.
20
21mod board;
22mod ffi;
23mod par;
24mod play;
25mod strain_flags;
26mod system_info;
27mod tricks;
28mod vulnerability;
29
30pub use board::*;
31pub use par::*;
32pub use play::*;
33pub use strain_flags::*;
34pub use system_info::*;
35pub use tricks::*;
36pub use vulnerability::*;
37
38use crate::deal::FullDeal;
39use crate::seat::Seat;
40
41use dds_bridge_sys as sys;
42use parking_lot::Mutex;
43
44use core::ffi::c_int;
45use core::mem::MaybeUninit;
46use std::sync::LazyLock;
47
48/// Maximum number of boards that can be solved in a single batch call to DDS
49///
50/// This is a hard limit in DDS, not a limit of this crate.  The batch methods
51/// in [`Solver`] will automatically split their input into segments of this
52/// size or smaller, so users of this crate don't need to worry about it as long
53/// as they use the batch methods for large inputs.  However, if users call the
54/// unsafe segment methods directly, they must ensure that their input sizes
55/// don't exceed this limit.
56///
57/// See also [`sys::MAXNOOFBOARDS`] and the safety requirements of the batch
58/// methods in [`Solver`].
59const MAX_BOARD_COUNT: usize = sys::MAXNOOFBOARDS as usize;
60
61/// Panics if `status` is negative, which indicates an error in DDS.  The panic
62/// message is a human-readable description of the error code returned by DDS.
63const fn check(status: i32) {
64    let msg: &[u8] = match status {
65        0.. => return,
66        sys::RETURN_ZERO_CARDS => sys::TEXT_ZERO_CARDS,
67        sys::RETURN_TARGET_TOO_HIGH => sys::TEXT_TARGET_TOO_HIGH,
68        sys::RETURN_DUPLICATE_CARDS => sys::TEXT_DUPLICATE_CARDS,
69        sys::RETURN_TARGET_WRONG_LO => sys::TEXT_TARGET_WRONG_LO,
70        sys::RETURN_TARGET_WRONG_HI => sys::TEXT_TARGET_WRONG_HI,
71        sys::RETURN_SOLNS_WRONG_LO => sys::TEXT_SOLNS_WRONG_LO,
72        sys::RETURN_SOLNS_WRONG_HI => sys::TEXT_SOLNS_WRONG_HI,
73        sys::RETURN_TOO_MANY_CARDS => sys::TEXT_TOO_MANY_CARDS,
74        sys::RETURN_SUIT_OR_RANK => sys::TEXT_SUIT_OR_RANK,
75        sys::RETURN_PLAYED_CARD => sys::TEXT_PLAYED_CARD,
76        sys::RETURN_CARD_COUNT => sys::TEXT_CARD_COUNT,
77        sys::RETURN_THREAD_INDEX => sys::TEXT_THREAD_INDEX,
78        sys::RETURN_MODE_WRONG_LO => sys::TEXT_MODE_WRONG_LO,
79        sys::RETURN_MODE_WRONG_HI => sys::TEXT_MODE_WRONG_HI,
80        sys::RETURN_TRUMP_WRONG => sys::TEXT_TRUMP_WRONG,
81        sys::RETURN_FIRST_WRONG => sys::TEXT_FIRST_WRONG,
82        sys::RETURN_PLAY_FAULT => sys::TEXT_PLAY_FAULT,
83        sys::RETURN_PBN_FAULT => sys::TEXT_PBN_FAULT,
84        sys::RETURN_TOO_MANY_BOARDS => sys::TEXT_TOO_MANY_BOARDS,
85        sys::RETURN_THREAD_CREATE => sys::TEXT_THREAD_CREATE,
86        sys::RETURN_THREAD_WAIT => sys::TEXT_THREAD_WAIT,
87        sys::RETURN_THREAD_MISSING => sys::TEXT_THREAD_MISSING,
88        sys::RETURN_NO_SUIT => sys::TEXT_NO_SUIT,
89        sys::RETURN_TOO_MANY_TABLES => sys::TEXT_TOO_MANY_TABLES,
90        sys::RETURN_CHUNK_SIZE => sys::TEXT_CHUNK_SIZE,
91        _ => sys::TEXT_UNKNOWN_FAULT,
92    };
93    // SAFETY: Error messages are ASCII literals in the C++ code of DDS.
94    panic!("{}", unsafe { core::str::from_utf8_unchecked(msg) });
95}
96
97/// Calculate par score and contracts for a deal
98///
99/// - `tricks`: The number of tricks each seat can take as declarer for each strain
100/// - `vul`: The vulnerability of pairs
101/// - `dealer`: The dealer of the deal
102///
103/// # Panics
104///
105/// Not expected — panics here are bugs. See the module-level panic policy.
106#[must_use]
107pub fn calculate_par(tricks: TrickCountTable, vul: Vulnerability, dealer: Seat) -> Par {
108    let mut par = sys::parResultsMaster::default();
109    let status = unsafe {
110        sys::DealerParBin(
111            &mut tricks.into(),
112            &raw mut par,
113            vul.to_sys(),
114            dealer as c_int,
115        )
116    };
117    check(status);
118    par.into()
119}
120
121/// Calculate par scores for both pairs
122///
123/// - `tricks`: The number of tricks each seat can take as declarer for each strain
124/// - `vul`: The vulnerability of pairs
125///
126/// # Panics
127///
128/// Not expected — panics here are bugs. See the module-level panic policy.
129#[must_use]
130pub fn calculate_pars(tricks: TrickCountTable, vul: Vulnerability) -> [Par; 2] {
131    let mut pars = [sys::parResultsMaster::default(); 2];
132    // SAFE: calculating par is reentrant
133    let status = unsafe { sys::SidesParBin(&mut tricks.into(), &raw mut pars[0], vul.to_sys()) };
134    check(status);
135    pars.map(Into::into)
136}
137
138static THREAD_POOL: LazyLock<Mutex<()>> = LazyLock::new(|| {
139    unsafe { sys::SetMaxThreads(0) };
140    Mutex::new(())
141});
142
143/// Exclusive handle to the DDS solver
144///
145/// DDS functions are not reentrant, so this struct holds a lock on the global
146/// thread pool.  Acquire a `Solver` once and call methods on it to avoid
147/// repeated locking.
148///
149/// The batch functions ([`CalcAllTables`](sys::CalcAllTables),
150/// [`SolveAllBoardsBin`](sys::SolveAllBoardsBin)) are internally
151/// multi-threaded, so parallelism is still utilized within each call.
152pub struct Solver(#[allow(dead_code)] parking_lot::MutexGuard<'static, ()>);
153
154impl Solver {
155    /// Acquire exclusive access to the DDS solver, blocking until available
156    #[must_use]
157    pub fn lock() -> Self {
158        Self(THREAD_POOL.lock())
159    }
160
161    /// Try to acquire exclusive access to the DDS solver without blocking
162    ///
163    /// Returns `None` if the solver is currently in use.
164    #[must_use]
165    pub fn try_lock() -> Option<Self> {
166        THREAD_POOL.try_lock().map(Self)
167    }
168
169    /// Get information about the underlying DDS library
170    #[must_use]
171    pub fn system_info(&self) -> SystemInfo {
172        let mut inner = MaybeUninit::uninit();
173        unsafe { sys::GetDDSInfo(inner.as_mut_ptr()) };
174        SystemInfo(unsafe { inner.assume_init() })
175    }
176
177    /// Solve a single deal with [`sys::CalcDDtable`]
178    ///
179    /// # Panics
180    ///
181    /// Not expected — panics here are bugs. See the module-level panic policy.
182    ///
183    /// # Examples
184    ///
185    /// ```
186    /// use dds_bridge::{FullDeal, Seat, Solver, Strain};
187    ///
188    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
189    /// // Each player holds a 13-card straight flush in one suit.
190    /// let deal: FullDeal = "N:AKQJT98765432... .AKQJT98765432.. \
191    ///                       ..AKQJT98765432. ...AKQJT98765432".parse()?;
192    /// let tricks = Solver::lock().solve_deal(deal);
193    /// // North holds all the spades, so North or South declaring spades
194    /// // draws trumps and takes every trick.
195    /// assert_eq!(u8::from(tricks[Strain::Spades].get(Seat::North)), 13);
196    /// # Ok(())
197    /// # }
198    /// ```
199    #[must_use]
200    pub fn solve_deal(&self, deal: FullDeal) -> TrickCountTable {
201        let mut result = sys::ddTableResults::default();
202        let status = unsafe { sys::CalcDDtable(deal.into(), &raw mut result) };
203        check(status);
204        result.into()
205    }
206
207    /// Solve deals with a single call of [`sys::CalcAllTables`]
208    ///
209    /// - `deals`: A slice of deals to solve
210    /// - `flags`: Flags of strains to solve for
211    ///
212    /// # Safety
213    ///
214    /// 1. **Thread-unsafe:** The caller must ensure that no other thread is
215    ///    calling any DDS function while this function is running.  This is
216    ///    automatically guaranteed if the caller acquires a `Solver` before
217    ///    calling this function.
218    /// 2. `deals.len() * flags.bits().count_ones()` must not exceed
219    ///    [`sys::MAXNOOFBOARDS`].
220    ///
221    unsafe fn solve_deal_segment(
222        deals: &[FullDeal],
223        flags: NonEmptyStrainFlags,
224    ) -> sys::ddTablesRes {
225        let flags = flags.get();
226        let strain_count = flags.bits().count_ones() as usize;
227
228        let mut pack = sys::ddTableDeals {
229            noOfTables: ffi::count_to_sys(deals.len(), MAX_BOARD_COUNT / strain_count),
230            ..Default::default()
231        };
232        deals
233            .iter()
234            .enumerate()
235            .for_each(|(i, &deal)| pack.deals[i] = deal.into());
236
237        let mut filter = [
238            c_int::from(!flags.contains(StrainFlags::SPADES)),
239            c_int::from(!flags.contains(StrainFlags::HEARTS)),
240            c_int::from(!flags.contains(StrainFlags::DIAMONDS)),
241            c_int::from(!flags.contains(StrainFlags::CLUBS)),
242            c_int::from(!flags.contains(StrainFlags::NOTRUMP)),
243        ];
244        let mut res = sys::ddTablesRes::default();
245        let status = unsafe {
246            sys::CalcAllTables(
247                &raw mut pack,
248                -1,
249                filter.as_mut_ptr(),
250                &raw mut res,
251                &mut sys::allParResults::default(),
252            )
253        };
254        check(status);
255        res
256    }
257
258    /// Solve deals in parallel for given strains
259    ///
260    /// - `deals`: A slice of deals to solve
261    /// - `flags`: Flags of strains to solve for
262    ///
263    /// # Panics
264    ///
265    /// Not expected — panics here are bugs. See the module-level panic policy.
266    #[must_use]
267    pub fn solve_deals(
268        &self,
269        deals: &[FullDeal],
270        flags: NonEmptyStrainFlags,
271    ) -> Vec<TrickCountTable> {
272        let mut tables = Vec::new();
273        for chunk in deals.chunks(MAX_BOARD_COUNT / flags.get().bits().count_ones() as usize) {
274            tables.extend(
275                unsafe { Self::solve_deal_segment(chunk, flags) }.results[..chunk.len()]
276                    .iter()
277                    .map(|&x| TrickCountTable::from(x)),
278            );
279        }
280        tables
281    }
282
283    /// Solve a single board with [`sys::SolveBoard`]
284    ///
285    /// # Panics
286    ///
287    /// Not expected — panics here are bugs. See the module-level panic policy.
288    #[must_use]
289    pub fn solve_board(&self, objective: Objective) -> FoundPlays {
290        let mut result = sys::futureTricks::default();
291        let status = unsafe {
292            sys::SolveBoard(
293                objective.board.into(),
294                objective.target.target(),
295                objective.target.solutions(),
296                0,
297                &raw mut result,
298                0,
299            )
300        };
301        check(status);
302        FoundPlays::from(result)
303    }
304
305    /// Solve boards with a single call of [`sys::SolveAllBoardsBin`]
306    ///
307    /// - `args`: A slice of objectives to solve
308    ///
309    /// # Safety
310    ///
311    /// 1. **Thread-unsafe:** The caller must ensure that no other thread is
312    ///    calling any DDS function while this function is running.  This is
313    ///    automatically guaranteed if the caller acquires a `Solver` before
314    ///    calling this function.
315    /// 2. `args.len()` must not exceed [`sys::MAXNOOFBOARDS`].
316    ///
317    unsafe fn solve_board_segment(args: &[Objective]) -> sys::solvedBoards {
318        let mut pack = sys::boards {
319            noOfBoards: ffi::count_to_sys(args.len(), MAX_BOARD_COUNT),
320            ..Default::default()
321        };
322        args.iter().enumerate().for_each(|(i, obj)| {
323            pack.deals[i] = obj.board.clone().into();
324            pack.target[i] = obj.target.target();
325            pack.solutions[i] = obj.target.solutions();
326        });
327        let mut res = sys::solvedBoards::default();
328        let status = unsafe { sys::SolveAllBoardsBin(&raw mut pack, &raw mut res) };
329        check(status);
330        res
331    }
332
333    /// Solve boards in parallel
334    ///
335    /// - `args`: A slice of boards and their targets to solve
336    ///
337    /// # Panics
338    ///
339    /// Not expected — panics here are bugs. See the module-level panic policy.
340    #[must_use]
341    pub fn solve_boards(&self, args: &[Objective]) -> Vec<FoundPlays> {
342        let mut solutions = Vec::new();
343        for chunk in args.chunks(MAX_BOARD_COUNT) {
344            solutions.extend(
345                unsafe { Self::solve_board_segment(chunk) }.solvedBoard[..chunk.len()]
346                    .iter()
347                    .map(|&x| FoundPlays::from(x)),
348            );
349        }
350        solutions
351    }
352
353    /// Trace DD trick counts before and after each played card with
354    /// [`sys::AnalysePlayBin`]
355    ///
356    /// # Panics
357    ///
358    /// Not expected — panics here are bugs. See the module-level panic policy.
359    #[must_use]
360    pub fn analyse_play(&self, trace: PlayTrace) -> PlayAnalysis {
361        let mut result = sys::solvedPlay::default();
362        let play = PlayTraceBin::from(&trace.cards);
363        let status = unsafe { sys::AnalysePlayBin(trace.board.into(), play.0, &raw mut result, 0) };
364        check(status);
365        PlayAnalysis::from(result)
366    }
367
368    /// Analyse play traces with a single call of [`sys::AnalyseAllPlaysBin`]
369    ///
370    /// # Safety
371    ///
372    /// 1. **Thread-unsafe:** The caller must ensure that no other thread is
373    ///    calling any DDS function while this function is running.  This is
374    ///    automatically guaranteed if the caller acquires a `Solver` before
375    ///    calling this function.
376    /// 2. `traces.len()` must not exceed [`sys::MAXNOOFBOARDS`].
377    ///
378    unsafe fn analyse_play_segment(traces: &[PlayTrace]) -> sys::solvedPlays {
379        let mut pack = sys::boards {
380            noOfBoards: ffi::count_to_sys(traces.len(), MAX_BOARD_COUNT),
381            ..Default::default()
382        };
383        let mut plays = sys::playTracesBin {
384            noOfBoards: ffi::count_to_sys(traces.len(), MAX_BOARD_COUNT),
385            ..Default::default()
386        };
387        traces.iter().enumerate().for_each(|(i, trace)| {
388            pack.deals[i] = trace.board.clone().into();
389            plays.plays[i] = PlayTraceBin::from(&trace.cards).0;
390        });
391        let mut res = sys::solvedPlays::default();
392        let status =
393            unsafe { sys::AnalyseAllPlaysBin(&raw mut pack, &raw mut plays, &raw mut res, 0) };
394        check(status);
395        res
396    }
397
398    /// Trace DD trick counts in parallel with [`sys::AnalyseAllPlaysBin`]
399    ///
400    /// # Panics
401    ///
402    /// Not expected — panics here are bugs. See the module-level panic policy.
403    #[must_use]
404    pub fn analyse_plays(&self, traces: &[PlayTrace]) -> Vec<PlayAnalysis> {
405        let mut results = Vec::new();
406        for chunk in traces.chunks(MAX_BOARD_COUNT) {
407            results.extend(
408                unsafe { Self::analyse_play_segment(chunk) }.solved[..chunk.len()]
409                    .iter()
410                    .map(|&x| PlayAnalysis::from(x)),
411            );
412        }
413        results
414    }
415}