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 =
110        unsafe { sys::DealerParBin(&tricks.into(), &raw mut par, vul.to_sys(), dealer as c_int) };
111    check(status);
112    par.into()
113}
114
115/// Calculate par scores for both pairs
116///
117/// - `tricks`: The number of tricks each seat can take as declarer for each strain
118/// - `vul`: The vulnerability of pairs
119///
120/// # Panics
121///
122/// Not expected — panics here are bugs. See the module-level panic policy.
123#[must_use]
124pub fn calculate_pars(tricks: TrickCountTable, vul: Vulnerability) -> [Par; 2] {
125    let mut pars = [sys::ParResultsMaster::default(); 2];
126    // SAFE: calculating par is reentrant
127    let status = unsafe { sys::SidesParBin(&tricks.into(), &raw mut pars[0], vul.to_sys()) };
128    check(status);
129    pars.map(Into::into)
130}
131
132static THREAD_POOL: LazyLock<Mutex<()>> = LazyLock::new(|| {
133    unsafe { sys::SetMaxThreads(0) };
134    Mutex::new(())
135});
136
137/// Exclusive handle to the DDS solver
138///
139/// DDS functions are not reentrant, so this struct holds a lock on the global
140/// thread pool.  Acquire a `Solver` once and call methods on it to avoid
141/// repeated locking.
142///
143/// The batch functions ([`CalcAllTables`](sys::CalcAllTables),
144/// [`SolveAllBoardsBin`](sys::SolveAllBoardsBin)) are internally
145/// multi-threaded, so parallelism is still utilized within each call.
146pub struct Solver(#[allow(dead_code)] parking_lot::MutexGuard<'static, ()>);
147
148impl Solver {
149    /// Acquire exclusive access to the DDS solver, blocking until available
150    #[must_use]
151    pub fn lock() -> Self {
152        Self(THREAD_POOL.lock())
153    }
154
155    /// Try to acquire exclusive access to the DDS solver without blocking
156    ///
157    /// Returns `None` if the solver is currently in use.
158    #[must_use]
159    pub fn try_lock() -> Option<Self> {
160        THREAD_POOL.try_lock().map(Self)
161    }
162
163    /// Get information about the underlying DDS library
164    #[must_use]
165    pub fn system_info(&self) -> SystemInfo {
166        let mut inner = MaybeUninit::uninit();
167        unsafe { sys::GetDDSInfo(inner.as_mut_ptr()) };
168        SystemInfo(unsafe { inner.assume_init() })
169    }
170
171    /// Solve a single deal with [`sys::CalcDDtable`]
172    ///
173    /// # Panics
174    ///
175    /// Not expected — panics here are bugs. See the module-level panic policy.
176    ///
177    /// # Examples
178    ///
179    /// ```
180    /// use dds_bridge::{FullDeal, Seat, Solver, Strain};
181    ///
182    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
183    /// // Each player holds a 13-card straight flush in one suit.
184    /// let deal: FullDeal = "N:AKQJT98765432... .AKQJT98765432.. \
185    ///                       ..AKQJT98765432. ...AKQJT98765432".parse()?;
186    /// let tricks = Solver::lock().solve_deal(deal);
187    /// // North holds all the spades, so North or South declaring spades
188    /// // draws trumps and takes every trick.
189    /// assert_eq!(u8::from(tricks[Strain::Spades].get(Seat::North)), 13);
190    /// # Ok(())
191    /// # }
192    /// ```
193    #[must_use]
194    pub fn solve_deal(&self, deal: FullDeal) -> TrickCountTable {
195        let mut result = sys::DdTableResults::default();
196        let status = unsafe { sys::CalcDDtable(deal.into(), &raw mut result) };
197        check(status);
198        result.into()
199    }
200
201    /// Solve deals with a single call of [`sys::CalcAllTables`]
202    ///
203    /// - `deals`: A slice of deals to solve
204    /// - `flags`: Flags of strains to solve for
205    ///
206    /// # Safety
207    ///
208    /// 1. **Thread-unsafe:** The caller must ensure that no other thread is
209    ///    calling any DDS function while this function is running.  This is
210    ///    automatically guaranteed if the caller acquires a `Solver` before
211    ///    calling this function.
212    /// 2. `deals.len() * flags.bits().count_ones()` must not exceed
213    ///    [`sys::MAXNOOFBOARDS`].
214    ///
215    unsafe fn solve_deal_segment(
216        deals: &[FullDeal],
217        flags: NonEmptyStrainFlags,
218    ) -> sys::DdTablesRes {
219        let flags = flags.get();
220        let strain_count = flags.bits().count_ones() as usize;
221
222        let mut pack = sys::DdTableDeals {
223            no_of_tables: ffi::count_to_sys(deals.len(), MAX_BOARD_COUNT / strain_count),
224            ..Default::default()
225        };
226        deals
227            .iter()
228            .enumerate()
229            .for_each(|(i, &deal)| pack.deals[i] = deal.into());
230
231        let mut filter = [
232            c_int::from(!flags.contains(StrainFlags::SPADES)),
233            c_int::from(!flags.contains(StrainFlags::HEARTS)),
234            c_int::from(!flags.contains(StrainFlags::DIAMONDS)),
235            c_int::from(!flags.contains(StrainFlags::CLUBS)),
236            c_int::from(!flags.contains(StrainFlags::NOTRUMP)),
237        ];
238        let mut res = sys::DdTablesRes::default();
239        let status = unsafe {
240            sys::CalcAllTables(
241                &raw mut pack,
242                -1,
243                filter.as_mut_ptr(),
244                &raw mut res,
245                &mut sys::AllParResults::default(),
246            )
247        };
248        check(status);
249        res
250    }
251
252    /// Solve deals in parallel for given strains
253    ///
254    /// - `deals`: A slice of deals to solve
255    /// - `flags`: Flags of strains to solve for
256    ///
257    /// # Panics
258    ///
259    /// Not expected — panics here are bugs. See the module-level panic policy.
260    #[must_use]
261    pub fn solve_deals(
262        &self,
263        deals: &[FullDeal],
264        flags: NonEmptyStrainFlags,
265    ) -> Vec<TrickCountTable> {
266        let mut tables = Vec::new();
267        for chunk in deals.chunks(MAX_BOARD_COUNT / flags.get().bits().count_ones() as usize) {
268            tables.extend(
269                unsafe { Self::solve_deal_segment(chunk, flags) }.results[..chunk.len()]
270                    .iter()
271                    .map(|&x| TrickCountTable::from(x)),
272            );
273        }
274        tables
275    }
276
277    /// Solve a single board with [`sys::SolveBoard`]
278    ///
279    /// # Panics
280    ///
281    /// Not expected — panics here are bugs. See the module-level panic policy.
282    #[must_use]
283    pub fn solve_board(&self, objective: Objective) -> FoundPlays {
284        let mut result = sys::FutureTricks::default();
285        let status = unsafe {
286            sys::SolveBoard(
287                objective.board.into(),
288                objective.target.target(),
289                objective.target.solutions(),
290                0,
291                &raw mut result,
292                0,
293            )
294        };
295        check(status);
296        FoundPlays::from(result)
297    }
298
299    /// Solve boards with a single call of [`sys::SolveAllBoardsBin`]
300    ///
301    /// - `args`: A slice of objectives to solve
302    ///
303    /// # Safety
304    ///
305    /// 1. **Thread-unsafe:** The caller must ensure that no other thread is
306    ///    calling any DDS function while this function is running.  This is
307    ///    automatically guaranteed if the caller acquires a `Solver` before
308    ///    calling this function.
309    /// 2. `args.len()` must not exceed [`sys::MAXNOOFBOARDS`].
310    ///
311    unsafe fn solve_board_segment(args: &[Objective]) -> sys::SolvedBoards {
312        let mut pack = sys::Boards {
313            no_of_boards: ffi::count_to_sys(args.len(), MAX_BOARD_COUNT),
314            ..Default::default()
315        };
316        args.iter().enumerate().for_each(|(i, obj)| {
317            pack.deals[i] = obj.board.clone().into();
318            pack.target[i] = obj.target.target();
319            pack.solutions[i] = obj.target.solutions();
320        });
321        let mut res = sys::SolvedBoards::default();
322        let status = unsafe { sys::SolveAllBoardsBin(&raw mut pack, &raw mut res) };
323        check(status);
324        res
325    }
326
327    /// Solve boards in parallel
328    ///
329    /// - `args`: A slice of boards and their targets to solve
330    ///
331    /// # Panics
332    ///
333    /// Not expected — panics here are bugs. See the module-level panic policy.
334    #[must_use]
335    pub fn solve_boards(&self, args: &[Objective]) -> Vec<FoundPlays> {
336        let mut solutions = Vec::new();
337        for chunk in args.chunks(MAX_BOARD_COUNT) {
338            solutions.extend(
339                unsafe { Self::solve_board_segment(chunk) }.solved_board[..chunk.len()]
340                    .iter()
341                    .map(|&x| FoundPlays::from(x)),
342            );
343        }
344        solutions
345    }
346
347    /// Trace DD trick counts before and after each played card with
348    /// [`sys::AnalysePlayBin`]
349    ///
350    /// # Panics
351    ///
352    /// Not expected — panics here are bugs. See the module-level panic policy.
353    #[must_use]
354    pub fn analyse_play(&self, trace: PlayTrace) -> PlayAnalysis {
355        let mut result = sys::SolvedPlay::default();
356        let play = PlayTraceBin::from(&trace.cards);
357        let status = unsafe { sys::AnalysePlayBin(trace.board.into(), play.0, &raw mut result, 0) };
358        check(status);
359        PlayAnalysis::from(result)
360    }
361
362    /// Analyse play traces with a single call of [`sys::AnalyseAllPlaysBin`]
363    ///
364    /// # Safety
365    ///
366    /// 1. **Thread-unsafe:** The caller must ensure that no other thread is
367    ///    calling any DDS function while this function is running.  This is
368    ///    automatically guaranteed if the caller acquires a `Solver` before
369    ///    calling this function.
370    /// 2. `traces.len()` must not exceed [`sys::MAXNOOFBOARDS`].
371    ///
372    unsafe fn analyse_play_segment(traces: &[PlayTrace]) -> sys::SolvedPlays {
373        let mut pack = sys::Boards {
374            no_of_boards: ffi::count_to_sys(traces.len(), MAX_BOARD_COUNT),
375            ..Default::default()
376        };
377        let mut plays = sys::PlayTracesBin {
378            no_of_boards: ffi::count_to_sys(traces.len(), MAX_BOARD_COUNT),
379            ..Default::default()
380        };
381        traces.iter().enumerate().for_each(|(i, trace)| {
382            pack.deals[i] = trace.board.clone().into();
383            plays.plays[i] = PlayTraceBin::from(&trace.cards).0;
384        });
385        let mut res = sys::SolvedPlays::default();
386        let status =
387            unsafe { sys::AnalyseAllPlaysBin(&raw mut pack, &raw mut plays, &raw mut res, 0) };
388        check(status);
389        res
390    }
391
392    /// Trace DD trick counts in parallel with [`sys::AnalyseAllPlaysBin`]
393    ///
394    /// # Panics
395    ///
396    /// Not expected — panics here are bugs. See the module-level panic policy.
397    #[must_use]
398    pub fn analyse_plays(&self, traces: &[PlayTrace]) -> Vec<PlayAnalysis> {
399        let mut results = Vec::new();
400        for chunk in traces.chunks(MAX_BOARD_COUNT) {
401            results.extend(
402                unsafe { Self::analyse_play_segment(chunk) }.solved[..chunk.len()]
403                    .iter()
404                    .map(|&x| PlayAnalysis::from(x)),
405            );
406        }
407        results
408    }
409}