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}