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}