dds_bridge/solver/play.rs
1//! Play-trace input and play-analysis output types
2
3use super::board::Board;
4use super::ffi;
5use super::tricks::TrickCount;
6use crate::hand::{Card, Holding};
7
8use arrayvec::ArrayVec;
9use core::ffi::c_int;
10use dds_bridge_sys as sys;
11
12/// A starting board and a sequence of cards played from it
13///
14/// Input to [`Solver::analyse_play`](super::Solver::analyse_play). The two
15/// fields split the position and the play-trace cleanly:
16///
17/// - [`board`](Self::board) is the snapshot from which analysis begins. It
18/// encodes the state at the start of a trick — possibly with up to three
19/// cards already on the table in [`Board::current_cards`] — and
20/// [`Board::remaining`] holds only the cards still in each hand. Cards
21/// from **previously completed tricks are not represented individually**;
22/// they are simply absent from `remaining`.
23/// - [`cards`](Self::cards) is the play trace to replay from that snapshot,
24/// in chronological order. The first card in `cards` is whichever card
25/// comes *after* any already in `board.current_cards` — it does **not**
26/// restart the trick or repeat prior history. Each card must be legal
27/// (follow suit when possible and be held by the player on turn).
28///
29/// `cards` may span trick boundaries; DDS tracks trick completion and whose
30/// lead follows internally. The trace length may be any value from `0` to
31/// `52`.
32#[derive(Debug, Clone, PartialEq, Eq, Hash)]
33pub struct PlayTrace {
34 /// Snapshot at the start of analysis: state at the start of the current
35 /// trick, plus any 0–3 cards already played to it via
36 /// [`Board::current_cards`]
37 pub board: Board,
38 /// Cards played after `board`, in chronological order; may cross tricks
39 pub cards: ArrayVec<Card, 52>,
40}
41
42/// Thin wrapper over [`sys::playTraceBin`] so we can impl conversions for it
43#[repr(transparent)]
44pub(super) struct PlayTraceBin(pub(super) sys::playTraceBin);
45
46impl From<&ArrayVec<Card, 52>> for PlayTraceBin {
47 fn from(cards: &ArrayVec<Card, 52>) -> Self {
48 let mut play = sys::playTraceBin {
49 // SAFETY: ArrayVec ensures the length is always in 0..=52
50 #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
51 number: cards.len() as c_int,
52 ..Default::default()
53 };
54
55 for (i, card) in cards.iter().enumerate() {
56 play.suit[i] = 3 - card.suit as c_int;
57 play.rank[i] = c_int::from(card.rank.get());
58 }
59 Self(play)
60 }
61}
62
63/// Double-dummy trick counts before and after each played card in a trace
64///
65/// Returned by [`Solver::analyse_play`](super::Solver::analyse_play). Trick
66/// counts are from the declarer's viewpoint: declarer is the right-hand
67/// opponent of the opening leader (the side to lead the very first trick in
68/// the starting [`Board`]).
69///
70/// `tricks[0]` is the DD value before any card in the trace is played.
71/// `tricks[i]` for `i > 0` is the DD value after the i-th card. A drop from
72/// `tricks[i - 1]` to `tricks[i]` means that card was a double-dummy mistake
73/// by the side to move at the time.
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct PlayAnalysis {
76 /// Trick counts — `cards.len() + 1` entries, starting with the position
77 /// before any card is played
78 pub tricks: ArrayVec<TrickCount, 53>,
79}
80
81impl From<sys::solvedPlay> for PlayAnalysis {
82 fn from(solved: sys::solvedPlay) -> Self {
83 let number = ffi::count_from_sys(solved.number, solved.tricks.len());
84 Self {
85 tricks: solved.tricks[..number]
86 .iter()
87 .copied()
88 .map(ffi::trick_count_from_sys)
89 .collect(),
90 }
91 }
92}
93
94/// A play and its consequences
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
96pub struct Play {
97 /// The card to play, the highest in a sequence
98 ///
99 /// For example, if the solution is to play a card from ♥KQJ, this field
100 /// would be ♥K.
101 pub card: Card,
102
103 /// Lower equals in the sequence
104 ///
105 /// Playing any card in a sequence is equal in bridge and many trick-taking
106 /// games. This field contains lower cards in the sequence as `card`. For
107 /// example, if the solution is to play KQJ, this field would contain QJ.
108 pub equals: Holding,
109
110 /// Tricks this play would score
111 pub score: TrickCount,
112}
113
114/// Solved plays for a board
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct FoundPlays {
117 /// The plays and their consequences
118 pub plays: ArrayVec<Play, 13>,
119 /// The number of nodes searched by the solver
120 pub nodes: u32,
121}
122
123impl From<sys::futureTricks> for FoundPlays {
124 fn from(future: sys::futureTricks) -> Self {
125 let cards = ffi::count_from_sys(future.cards, future.suit.len());
126 let plays = (0..cards)
127 .map(|i| Play {
128 card: Card {
129 suit: ffi::suit_from_desc_index(future.suit[i]),
130 rank: ffi::rank_from_sys(future.rank[i]),
131 },
132 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
133 equals: Holding::from_bits_truncate(future.equals[i] as u16),
134 score: ffi::trick_count_from_sys(future.score[i]),
135 })
136 .collect();
137
138 Self {
139 plays,
140 #[allow(clippy::cast_sign_loss)]
141 nodes: future.nodes as u32,
142 }
143 }
144}