dds-bridge 0.18.0

Rusty API for DDS, the double dummy solver for bridge
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
//! Double-dummy solver and par-calculation bindings built on [`dds_bridge_sys`].
//!
//! # Panic policy
//!
//! The solver entry points in this module — [`calculate_par`],
//! [`calculate_pars`], and the [`Solver`] methods
//! [`solve_deal`](Solver::solve_deal), [`solve_deals`](Solver::solve_deals),
//! [`solve_board`](Solver::solve_board), [`solve_boards`](Solver::solve_boards),
//! [`analyse_play`](Solver::analyse_play), and
//! [`analyse_plays`](Solver::analyse_plays) — are not expected to panic.
//! They map DDS status codes through an internal helper that panics on error,
//! but reaching that panic means either invalid input slipped past a safe
//! constructor or DDS itself misbehaved. Either case is a bug — please report
//! it.
//!
//! This policy does not cover validator panics from safe constructors
//! (e.g. [`TrickCountRow::new`](crate::solver::TrickCountRow::new)), which
//! panic by design on out-of-range inputs and have `try_*` counterparts for
//! fallible construction.

mod board;
mod ffi;
mod par;
mod play;
mod strain_flags;
mod system_info;
mod tricks;
mod vulnerability;

pub use board::*;
pub use par::*;
pub use play::*;
pub use strain_flags::*;
pub use system_info::*;
pub use tricks::*;
pub use vulnerability::*;

use crate::deal::FullDeal;
use crate::seat::Seat;

use dds_bridge_sys as sys;
use parking_lot::Mutex;

use core::ffi::c_int;
use core::mem::MaybeUninit;
use std::sync::LazyLock;

/// Maximum number of boards that can be solved in a single batch call to DDS
///
/// This is a hard limit in DDS, not a limit of this crate.  The batch methods
/// in [`Solver`] will automatically split their input into segments of this
/// size or smaller, so users of this crate don't need to worry about it as long
/// as they use the batch methods for large inputs.  However, if users call the
/// unsafe segment methods directly, they must ensure that their input sizes
/// don't exceed this limit.
///
/// See also [`sys::MAXNOOFBOARDS`] and the safety requirements of the batch
/// methods in [`Solver`].
const MAX_BOARD_COUNT: usize = sys::MAXNOOFBOARDS as usize;

/// Panics if `status` is negative, which indicates an error in DDS.  The panic
/// message is a human-readable description of the error code returned by DDS.
const fn check(status: i32) {
    let msg: &[u8] = match status {
        0.. => return,
        sys::RETURN_ZERO_CARDS => sys::TEXT_ZERO_CARDS,
        sys::RETURN_TARGET_TOO_HIGH => sys::TEXT_TARGET_TOO_HIGH,
        sys::RETURN_DUPLICATE_CARDS => sys::TEXT_DUPLICATE_CARDS,
        sys::RETURN_TARGET_WRONG_LO => sys::TEXT_TARGET_WRONG_LO,
        sys::RETURN_TARGET_WRONG_HI => sys::TEXT_TARGET_WRONG_HI,
        sys::RETURN_SOLNS_WRONG_LO => sys::TEXT_SOLNS_WRONG_LO,
        sys::RETURN_SOLNS_WRONG_HI => sys::TEXT_SOLNS_WRONG_HI,
        sys::RETURN_TOO_MANY_CARDS => sys::TEXT_TOO_MANY_CARDS,
        sys::RETURN_SUIT_OR_RANK => sys::TEXT_SUIT_OR_RANK,
        sys::RETURN_PLAYED_CARD => sys::TEXT_PLAYED_CARD,
        sys::RETURN_CARD_COUNT => sys::TEXT_CARD_COUNT,
        sys::RETURN_THREAD_INDEX => sys::TEXT_THREAD_INDEX,
        sys::RETURN_MODE_WRONG_LO => sys::TEXT_MODE_WRONG_LO,
        sys::RETURN_MODE_WRONG_HI => sys::TEXT_MODE_WRONG_HI,
        sys::RETURN_TRUMP_WRONG => sys::TEXT_TRUMP_WRONG,
        sys::RETURN_FIRST_WRONG => sys::TEXT_FIRST_WRONG,
        sys::RETURN_PLAY_FAULT => sys::TEXT_PLAY_FAULT,
        sys::RETURN_PBN_FAULT => sys::TEXT_PBN_FAULT,
        sys::RETURN_TOO_MANY_BOARDS => sys::TEXT_TOO_MANY_BOARDS,
        sys::RETURN_THREAD_CREATE => sys::TEXT_THREAD_CREATE,
        sys::RETURN_THREAD_WAIT => sys::TEXT_THREAD_WAIT,
        sys::RETURN_THREAD_MISSING => sys::TEXT_THREAD_MISSING,
        sys::RETURN_NO_SUIT => sys::TEXT_NO_SUIT,
        sys::RETURN_TOO_MANY_TABLES => sys::TEXT_TOO_MANY_TABLES,
        sys::RETURN_CHUNK_SIZE => sys::TEXT_CHUNK_SIZE,
        _ => sys::TEXT_UNKNOWN_FAULT,
    };
    // SAFETY: Error messages are ASCII literals in the C++ code of DDS.
    panic!("{}", unsafe { core::str::from_utf8_unchecked(msg) });
}

/// Calculate par score and contracts for a deal
///
/// - `tricks`: The number of tricks each seat can take as declarer for each strain
/// - `vul`: The vulnerability of pairs
/// - `dealer`: The dealer of the deal
///
/// # Panics
///
/// Not expected — panics here are bugs. See the module-level panic policy.
#[must_use]
pub fn calculate_par(tricks: TrickCountTable, vul: Vulnerability, dealer: Seat) -> Par {
    let mut par = sys::parResultsMaster::default();
    let status = unsafe {
        sys::DealerParBin(
            &mut tricks.into(),
            &raw mut par,
            vul.to_sys(),
            dealer as c_int,
        )
    };
    check(status);
    par.into()
}

/// Calculate par scores for both pairs
///
/// - `tricks`: The number of tricks each seat can take as declarer for each strain
/// - `vul`: The vulnerability of pairs
///
/// # Panics
///
/// Not expected — panics here are bugs. See the module-level panic policy.
#[must_use]
pub fn calculate_pars(tricks: TrickCountTable, vul: Vulnerability) -> [Par; 2] {
    let mut pars = [sys::parResultsMaster::default(); 2];
    // SAFE: calculating par is reentrant
    let status = unsafe { sys::SidesParBin(&mut tricks.into(), &raw mut pars[0], vul.to_sys()) };
    check(status);
    pars.map(Into::into)
}

static THREAD_POOL: LazyLock<Mutex<()>> = LazyLock::new(|| {
    unsafe { sys::SetMaxThreads(0) };
    Mutex::new(())
});

/// Exclusive handle to the DDS solver
///
/// DDS functions are not reentrant, so this struct holds a lock on the global
/// thread pool.  Acquire a `Solver` once and call methods on it to avoid
/// repeated locking.
///
/// The batch functions ([`CalcAllTables`](sys::CalcAllTables),
/// [`SolveAllBoardsBin`](sys::SolveAllBoardsBin)) are internally
/// multi-threaded, so parallelism is still utilized within each call.
pub struct Solver(#[allow(dead_code)] parking_lot::MutexGuard<'static, ()>);

impl Solver {
    /// Acquire exclusive access to the DDS solver, blocking until available
    #[must_use]
    pub fn lock() -> Self {
        Self(THREAD_POOL.lock())
    }

    /// Try to acquire exclusive access to the DDS solver without blocking
    ///
    /// Returns `None` if the solver is currently in use.
    #[must_use]
    pub fn try_lock() -> Option<Self> {
        THREAD_POOL.try_lock().map(Self)
    }

    /// Get information about the underlying DDS library
    #[must_use]
    pub fn system_info(&self) -> SystemInfo {
        let mut inner = MaybeUninit::uninit();
        unsafe { sys::GetDDSInfo(inner.as_mut_ptr()) };
        SystemInfo(unsafe { inner.assume_init() })
    }

    /// Solve a single deal with [`sys::CalcDDtable`]
    ///
    /// # Panics
    ///
    /// Not expected — panics here are bugs. See the module-level panic policy.
    ///
    /// # Examples
    ///
    /// ```
    /// use dds_bridge::{FullDeal, Seat, Solver, Strain};
    ///
    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// // Each player holds a 13-card straight flush in one suit.
    /// let deal: FullDeal = "N:AKQJT98765432... .AKQJT98765432.. \
    ///                       ..AKQJT98765432. ...AKQJT98765432".parse()?;
    /// let tricks = Solver::lock().solve_deal(deal);
    /// // North holds all the spades, so North or South declaring spades
    /// // draws trumps and takes every trick.
    /// assert_eq!(u8::from(tricks[Strain::Spades].get(Seat::North)), 13);
    /// # Ok(())
    /// # }
    /// ```
    #[must_use]
    pub fn solve_deal(&self, deal: FullDeal) -> TrickCountTable {
        let mut result = sys::ddTableResults::default();
        let status = unsafe { sys::CalcDDtable(deal.into(), &raw mut result) };
        check(status);
        result.into()
    }

    /// Solve deals with a single call of [`sys::CalcAllTables`]
    ///
    /// - `deals`: A slice of deals to solve
    /// - `flags`: Flags of strains to solve for
    ///
    /// # Safety
    ///
    /// 1. **Thread-unsafe:** The caller must ensure that no other thread is
    ///    calling any DDS function while this function is running.  This is
    ///    automatically guaranteed if the caller acquires a `Solver` before
    ///    calling this function.
    /// 2. `deals.len() * flags.bits().count_ones()` must not exceed
    ///    [`sys::MAXNOOFBOARDS`].
    ///
    unsafe fn solve_deal_segment(
        deals: &[FullDeal],
        flags: NonEmptyStrainFlags,
    ) -> sys::ddTablesRes {
        let flags = flags.get();
        let strain_count = flags.bits().count_ones() as usize;

        let mut pack = sys::ddTableDeals {
            noOfTables: ffi::count_to_sys(deals.len(), MAX_BOARD_COUNT / strain_count),
            ..Default::default()
        };
        deals
            .iter()
            .enumerate()
            .for_each(|(i, &deal)| pack.deals[i] = deal.into());

        let mut filter = [
            c_int::from(!flags.contains(StrainFlags::SPADES)),
            c_int::from(!flags.contains(StrainFlags::HEARTS)),
            c_int::from(!flags.contains(StrainFlags::DIAMONDS)),
            c_int::from(!flags.contains(StrainFlags::CLUBS)),
            c_int::from(!flags.contains(StrainFlags::NOTRUMP)),
        ];
        let mut res = sys::ddTablesRes::default();
        let status = unsafe {
            sys::CalcAllTables(
                &raw mut pack,
                -1,
                filter.as_mut_ptr(),
                &raw mut res,
                &mut sys::allParResults::default(),
            )
        };
        check(status);
        res
    }

    /// Solve deals in parallel for given strains
    ///
    /// - `deals`: A slice of deals to solve
    /// - `flags`: Flags of strains to solve for
    ///
    /// # Panics
    ///
    /// Not expected — panics here are bugs. See the module-level panic policy.
    #[must_use]
    pub fn solve_deals(
        &self,
        deals: &[FullDeal],
        flags: NonEmptyStrainFlags,
    ) -> Vec<TrickCountTable> {
        let mut tables = Vec::new();
        for chunk in deals.chunks(MAX_BOARD_COUNT / flags.get().bits().count_ones() as usize) {
            tables.extend(
                unsafe { Self::solve_deal_segment(chunk, flags) }.results[..chunk.len()]
                    .iter()
                    .map(|&x| TrickCountTable::from(x)),
            );
        }
        tables
    }

    /// Solve a single board with [`sys::SolveBoard`]
    ///
    /// # Panics
    ///
    /// Not expected — panics here are bugs. See the module-level panic policy.
    #[must_use]
    pub fn solve_board(&self, objective: Objective) -> FoundPlays {
        let mut result = sys::futureTricks::default();
        let status = unsafe {
            sys::SolveBoard(
                objective.board.into(),
                objective.target.target(),
                objective.target.solutions(),
                0,
                &raw mut result,
                0,
            )
        };
        check(status);
        FoundPlays::from(result)
    }

    /// Solve boards with a single call of [`sys::SolveAllBoardsBin`]
    ///
    /// - `args`: A slice of objectives to solve
    ///
    /// # Safety
    ///
    /// 1. **Thread-unsafe:** The caller must ensure that no other thread is
    ///    calling any DDS function while this function is running.  This is
    ///    automatically guaranteed if the caller acquires a `Solver` before
    ///    calling this function.
    /// 2. `args.len()` must not exceed [`sys::MAXNOOFBOARDS`].
    ///
    unsafe fn solve_board_segment(args: &[Objective]) -> sys::solvedBoards {
        let mut pack = sys::boards {
            noOfBoards: ffi::count_to_sys(args.len(), MAX_BOARD_COUNT),
            ..Default::default()
        };
        args.iter().enumerate().for_each(|(i, obj)| {
            pack.deals[i] = obj.board.clone().into();
            pack.target[i] = obj.target.target();
            pack.solutions[i] = obj.target.solutions();
        });
        let mut res = sys::solvedBoards::default();
        let status = unsafe { sys::SolveAllBoardsBin(&raw mut pack, &raw mut res) };
        check(status);
        res
    }

    /// Solve boards in parallel
    ///
    /// - `args`: A slice of boards and their targets to solve
    ///
    /// # Panics
    ///
    /// Not expected — panics here are bugs. See the module-level panic policy.
    #[must_use]
    pub fn solve_boards(&self, args: &[Objective]) -> Vec<FoundPlays> {
        let mut solutions = Vec::new();
        for chunk in args.chunks(MAX_BOARD_COUNT) {
            solutions.extend(
                unsafe { Self::solve_board_segment(chunk) }.solvedBoard[..chunk.len()]
                    .iter()
                    .map(|&x| FoundPlays::from(x)),
            );
        }
        solutions
    }

    /// Trace DD trick counts before and after each played card with
    /// [`sys::AnalysePlayBin`]
    ///
    /// # Panics
    ///
    /// Not expected — panics here are bugs. See the module-level panic policy.
    #[must_use]
    pub fn analyse_play(&self, trace: PlayTrace) -> PlayAnalysis {
        let mut result = sys::solvedPlay::default();
        let play = PlayTraceBin::from(&trace.cards);
        let status = unsafe { sys::AnalysePlayBin(trace.board.into(), play.0, &raw mut result, 0) };
        check(status);
        PlayAnalysis::from(result)
    }

    /// Analyse play traces with a single call of [`sys::AnalyseAllPlaysBin`]
    ///
    /// # Safety
    ///
    /// 1. **Thread-unsafe:** The caller must ensure that no other thread is
    ///    calling any DDS function while this function is running.  This is
    ///    automatically guaranteed if the caller acquires a `Solver` before
    ///    calling this function.
    /// 2. `traces.len()` must not exceed [`sys::MAXNOOFBOARDS`].
    ///
    unsafe fn analyse_play_segment(traces: &[PlayTrace]) -> sys::solvedPlays {
        let mut pack = sys::boards {
            noOfBoards: ffi::count_to_sys(traces.len(), MAX_BOARD_COUNT),
            ..Default::default()
        };
        let mut plays = sys::playTracesBin {
            noOfBoards: ffi::count_to_sys(traces.len(), MAX_BOARD_COUNT),
            ..Default::default()
        };
        traces.iter().enumerate().for_each(|(i, trace)| {
            pack.deals[i] = trace.board.clone().into();
            plays.plays[i] = PlayTraceBin::from(&trace.cards).0;
        });
        let mut res = sys::solvedPlays::default();
        let status =
            unsafe { sys::AnalyseAllPlaysBin(&raw mut pack, &raw mut plays, &raw mut res, 0) };
        check(status);
        res
    }

    /// Trace DD trick counts in parallel with [`sys::AnalyseAllPlaysBin`]
    ///
    /// # Panics
    ///
    /// Not expected — panics here are bugs. See the module-level panic policy.
    #[must_use]
    pub fn analyse_plays(&self, traces: &[PlayTrace]) -> Vec<PlayAnalysis> {
        let mut results = Vec::new();
        for chunk in traces.chunks(MAX_BOARD_COUNT) {
            results.extend(
                unsafe { Self::analyse_play_segment(chunk) }.solved[..chunk.len()]
                    .iter()
                    .map(|&x| PlayAnalysis::from(x)),
            );
        }
        results
    }
}