Skip to main content

dds_bridge/solver/
tricks.rs

1//! Trick counts and their conversions to/from DDS FFI types
2
3use crate::deal::{Builder, FullDeal, PartialDeal};
4use crate::seat::Seat;
5use crate::{Strain, Suit};
6
7use dds_bridge_sys as sys;
8use thiserror::Error;
9
10use core::ffi::c_int;
11use core::fmt;
12
13/// Error returned when a trick count is outside `0..=13`
14///
15/// Produced by both [`TrickCount::try_new`] and [`TrickCountRow::try_new`].
16#[derive(Debug, Error, Clone, Copy, PartialEq, Eq, Hash)]
17#[error("trick count must be in 0..=13")]
18pub struct InvalidTrickCount;
19
20/// A number of tricks in `0..=13`
21///
22/// A validated newtype over `u8`, analogous to [`Level`](crate::contract::Level)
23/// (1..=7) and [`Rank`](crate::hand::Rank) (2..=14). Appears as the per-seat
24/// value returned by [`TrickCountRow::get`].
25#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
26#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
27#[cfg_attr(feature = "serde", serde(transparent))]
28#[repr(transparent)]
29pub struct TrickCount(u8);
30
31impl TrickCount {
32    /// Create a new trick count
33    ///
34    /// # Panics
35    ///
36    /// When `n` is outside `0..=13`. In const contexts, this is a compile-time
37    /// error.
38    #[must_use]
39    #[inline]
40    pub const fn new(n: u8) -> Self {
41        match Self::try_new(n) {
42            Ok(tc) => tc,
43            Err(_) => panic!("trick count must be in 0..=13"),
44        }
45    }
46
47    /// Try to create a new trick count
48    ///
49    /// # Errors
50    ///
51    /// When `n` is outside `0..=13`.
52    #[inline]
53    pub const fn try_new(n: u8) -> Result<Self, InvalidTrickCount> {
54        if n > 13 {
55            return Err(InvalidTrickCount);
56        }
57        Ok(Self(n))
58    }
59
60    /// Get the underlying `u8`
61    #[must_use]
62    #[inline]
63    pub const fn get(self) -> u8 {
64        self.0
65    }
66}
67
68impl From<TrickCount> for u8 {
69    #[inline]
70    fn from(tc: TrickCount) -> Self {
71        tc.0
72    }
73}
74
75impl From<TrickCount> for usize {
76    #[inline]
77    fn from(tc: TrickCount) -> Self {
78        tc.0 as Self
79    }
80}
81
82impl fmt::Display for TrickCount {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        self.0.fmt(f)
85    }
86}
87
88/// Tricks that each seat can take as declarer for a strain
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
90#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
91#[cfg_attr(feature = "serde", serde(transparent))]
92#[repr(transparent)]
93pub struct TrickCountRow(u16);
94
95impl TrickCountRow {
96    /// Create a new row from the number of tricks each seat can take
97    ///
98    /// # Panics
99    ///
100    /// When any value is outside `0..=13`.  In const contexts, this is a
101    /// compile-time error.
102    #[must_use]
103    #[inline]
104    pub const fn new(n: u8, e: u8, s: u8, w: u8) -> Self {
105        match Self::try_new(n, e, s, w) {
106            Ok(row) => row,
107            Err(_) => panic!("trick count must be in 0..=13"),
108        }
109    }
110
111    /// Try to create a new row from the number of tricks each seat can take
112    ///
113    /// # Errors
114    ///
115    /// When any value is outside `0..=13`.
116    #[inline]
117    pub const fn try_new(n: u8, e: u8, s: u8, w: u8) -> Result<Self, InvalidTrickCount> {
118        if n > 13 || e > 13 || s > 13 || w > 13 {
119            return Err(InvalidTrickCount);
120        }
121        Ok(Self(
122            (n as u16) << (4 * Seat::North as u8)
123                | (e as u16) << (4 * Seat::East as u8)
124                | (s as u16) << (4 * Seat::South as u8)
125                | (w as u16) << (4 * Seat::West as u8),
126        ))
127    }
128
129    /// Get the number of tricks a seat can take as declarer
130    #[must_use]
131    pub const fn get(self, seat: Seat) -> TrickCount {
132        TrickCount((self.0 >> (4 * seat as u8) & 0xF) as u8)
133    }
134
135    /// Hexadecimal representation from a seat's perspective
136    #[must_use]
137    pub const fn hex(self, seat: Seat) -> TrickCountRowHex {
138        TrickCountRowHex { row: self, seat }
139    }
140}
141
142/// Hexadecimal view of a [`TrickCountRow`] from a seat's perspective
143///
144/// Returned by [`TrickCountRow::hex`]. Formats as four hex digits — the tricks
145/// taken by the seat, its LHO, its partner, and its RHO — via the
146/// [`UpperHex`](fmt::UpperHex) impl.
147#[derive(Debug, Clone, Copy)]
148pub struct TrickCountRowHex {
149    row: TrickCountRow,
150    seat: Seat,
151}
152
153impl fmt::UpperHex for TrickCountRowHex {
154    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
155        write!(
156            f,
157            "{:X}{:X}{:X}{:X}",
158            self.row.get(self.seat).get(),
159            self.row.get(self.seat.lho()).get(),
160            self.row.get(self.seat.partner()).get(),
161            self.row.get(self.seat.rho()).get(),
162        )
163    }
164}
165
166/// Tricks that each seat can take as declarer for all strains
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
168#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
169#[cfg_attr(feature = "serde", serde(transparent))]
170#[repr(transparent)]
171pub struct TrickCountTable(pub [TrickCountRow; 5]);
172
173impl core::ops::Index<Strain> for TrickCountTable {
174    type Output = TrickCountRow;
175
176    fn index(&self, strain: Strain) -> &TrickCountRow {
177        &self.0[strain as usize]
178    }
179}
180
181impl TrickCountTable {
182    /// Hexadecimal representation from a seat's perspective
183    #[must_use]
184    pub const fn hex<T: AsRef<[Strain]>>(self, seat: Seat, strains: T) -> TrickCountTableHex<T> {
185        TrickCountTableHex {
186            table: self,
187            seat,
188            strains,
189        }
190    }
191}
192
193/// Hexadecimal view of a [`TrickCountTable`] from a seat's perspective
194///
195/// Returned by [`TrickCountTable::hex`]. Formats as one [`TrickCountRowHex`]
196/// per strain in the supplied slice, concatenated, via the
197/// [`UpperHex`](fmt::UpperHex) impl.
198#[derive(Debug, Clone, Copy)]
199pub struct TrickCountTableHex<T: AsRef<[Strain]>> {
200    table: TrickCountTable,
201    seat: Seat,
202    strains: T,
203}
204
205impl<T: AsRef<[Strain]>> fmt::UpperHex for TrickCountTableHex<T> {
206    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
207        for &strain in self.strains.as_ref() {
208            self.table[strain].hex(self.seat).fmt(f)?;
209        }
210        Ok(())
211    }
212}
213
214impl Strain {
215    /// Convert to the index in [`dds_bridge_sys`]
216    #[must_use]
217    const fn to_sys(self) -> usize {
218        match self {
219            Self::Spades => 0,
220            Self::Hearts => 1,
221            Self::Diamonds => 2,
222            Self::Clubs => 3,
223            Self::Notrump => 4,
224        }
225    }
226}
227
228impl From<sys::ddTableResults> for TrickCountTable {
229    fn from(table: sys::ddTableResults) -> Self {
230        use super::ffi::trick_count_from_sys;
231        let row = |r: [c_int; 4]| {
232            TrickCountRow::new(
233                trick_count_from_sys(r[0]).get(),
234                trick_count_from_sys(r[1]).get(),
235                trick_count_from_sys(r[2]).get(),
236                trick_count_from_sys(r[3]).get(),
237            )
238        };
239
240        Self([
241            row(table.resTable[Strain::Clubs.to_sys()]),
242            row(table.resTable[Strain::Diamonds.to_sys()]),
243            row(table.resTable[Strain::Hearts.to_sys()]),
244            row(table.resTable[Strain::Spades.to_sys()]),
245            row(table.resTable[Strain::Notrump.to_sys()]),
246        ])
247    }
248}
249
250impl From<TrickCountTable> for sys::ddTableResults {
251    fn from(table: TrickCountTable) -> Self {
252        const fn make_row(row: TrickCountRow) -> [c_int; 4] {
253            [
254                row.get(Seat::North).get() as c_int,
255                row.get(Seat::East).get() as c_int,
256                row.get(Seat::South).get() as c_int,
257                row.get(Seat::West).get() as c_int,
258            ]
259        }
260
261        Self {
262            resTable: [
263                make_row(table[Strain::Spades]),
264                make_row(table[Strain::Hearts]),
265                make_row(table[Strain::Diamonds]),
266                make_row(table[Strain::Clubs]),
267                make_row(table[Strain::Notrump]),
268            ],
269        }
270    }
271}
272
273/// FFI converter for a [`Builder`].  Used internally by [`FullDeal`] and
274/// [`PartialDeal`] converters; `Builder` itself is unvalidated so prefer those.
275impl From<Builder> for sys::ddTableDeal {
276    fn from(builder: Builder) -> Self {
277        Self {
278            cards: Seat::ALL.map(|seat| {
279                let hand = builder[seat];
280                [
281                    hand[Suit::Spades].to_bits().into(),
282                    hand[Suit::Hearts].to_bits().into(),
283                    hand[Suit::Diamonds].to_bits().into(),
284                    hand[Suit::Clubs].to_bits().into(),
285                ]
286            }),
287        }
288    }
289}
290
291impl From<FullDeal> for sys::ddTableDeal {
292    #[inline]
293    fn from(deal: FullDeal) -> Self {
294        Builder::from(deal).into()
295    }
296}
297
298impl From<PartialDeal> for sys::ddTableDeal {
299    #[inline]
300    fn from(subset: PartialDeal) -> Self {
301        Builder::from(subset).into()
302    }
303}