Skip to main content

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}