Skip to main content

dds_bridge/
deal.rs

1//! Four-hand deal containers.
2//!
3//! Seats and the bitset over them live in [`crate::seat`]; a [`Seat`] is the
4//! indexing key for every container in this module.
5//!
6//! Deals come in three flavors, distinguished by their invariants:
7//!
8//! * [`Builder`] — a mutable four-hand scratchpad with no invariants.  The
9//!   only deal type exposing [`IndexMut`](core::ops::IndexMut).
10//! * [`PartialDeal`] — validated and read-only: each hand holds at most 13
11//!   cards and the four hands are pairwise disjoint.
12//! * [`FullDeal`] — validated and read-only: exactly 13 cards per hand, all
13//!   52 cards accounted for.
14//!
15//! Build a validated deal via [`Builder::build_partial`] or
16//! [`Builder::build_full`]; both return the original `Builder` unchanged as
17//! the error on validation failure.  To mutate an already-validated deal,
18//! widen it back to a [`Builder`] and re-validate.
19//!
20//! All three deal types parse the [PBN] deal format —
21//! `<dealer>:<hand> <hand> <hand> <hand>` — with holdings ordered spades,
22//! hearts, diamonds, clubs.  `PartialDeal` additionally accepts relaxed hand
23//! sizes and `x` for unknown ranks.
24//!
25//! [PBN]: https://www.tistis.nl/pbn/
26
27use crate::hand::{Hand, ParseHandError};
28use crate::seat::Seat;
29use core::fmt::{self, Write as _};
30use core::ops;
31use core::str::FromStr;
32use thiserror::Error;
33
34/// An error which can be returned when parsing a [`PartialDeal`] or [`FullDeal`]
35#[derive(Debug, Error, Clone, Copy, PartialEq, Eq, Hash)]
36#[non_exhaustive]
37pub enum ParseDealError {
38    /// Invalid dealer tag
39    #[error("Invalid dealer tag for a deal")]
40    InvalidDealer,
41
42    /// Error in a hand
43    #[error(transparent)]
44    Hand(#[from] ParseHandError),
45
46    /// The deal does not contain 4 hands
47    #[error("The deal does not contain 4 hands")]
48    NotFourHands,
49
50    /// The deal is not a valid [`PartialDeal`]: some hand has more than 13 cards or
51    /// two hands share a card
52    #[error("The deal is not a valid subset (>13 cards per hand or overlapping hands)")]
53    InvalidPartialDeal,
54
55    /// The deal is not a [`FullDeal`]: some hand does not have exactly 13 cards
56    #[error("The deal is not a full deal (each hand must have exactly 13 cards)")]
57    NotFullDeal,
58}
59
60/// A loose deal builder — any combination of four hands, no invariants
61///
62/// Use `Builder` to construct a deal incrementally.  Convert it into a
63/// [`PartialDeal`] or [`FullDeal`] (via the inherent [`build_partial`] /
64/// [`build_full`] methods, or via [`TryFrom`]) once the hands are finalized.
65/// `Builder` is the only deal type that exposes [`IndexMut`](ops::IndexMut)
66/// for in-place mutation.
67///
68/// [`build_partial`]: Builder::build_partial
69/// [`build_full`]: Builder::build_full
70#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
71pub struct Builder([Hand; 4]);
72
73impl IntoIterator for Builder {
74    type Item = Hand;
75    type IntoIter = core::array::IntoIter<Hand, 4>;
76
77    #[inline]
78    fn into_iter(self) -> Self::IntoIter {
79        self.0.into_iter()
80    }
81}
82
83impl ops::Index<Seat> for Builder {
84    type Output = Hand;
85
86    #[inline]
87    fn index(&self, seat: Seat) -> &Hand {
88        &self.0[seat as usize]
89    }
90}
91
92impl ops::IndexMut<Seat> for Builder {
93    #[inline]
94    fn index_mut(&mut self, seat: Seat) -> &mut Hand {
95        &mut self.0[seat as usize]
96    }
97}
98
99impl Builder {
100    /// Construct an empty builder — all four hands empty
101    #[must_use]
102    pub const fn new() -> Self {
103        Self([Hand::EMPTY; 4])
104    }
105
106    /// Set the hand at [`Seat::North`]
107    #[must_use]
108    pub const fn north(mut self, hand: Hand) -> Self {
109        self.0[Seat::North as usize] = hand;
110        self
111    }
112
113    /// Set the hand at [`Seat::East`]
114    #[must_use]
115    pub const fn east(mut self, hand: Hand) -> Self {
116        self.0[Seat::East as usize] = hand;
117        self
118    }
119
120    /// Set the hand at [`Seat::South`]
121    #[must_use]
122    pub const fn south(mut self, hand: Hand) -> Self {
123        self.0[Seat::South as usize] = hand;
124        self
125    }
126
127    /// Set the hand at [`Seat::West`]
128    #[must_use]
129    pub const fn west(mut self, hand: Hand) -> Self {
130        self.0[Seat::West as usize] = hand;
131        self
132    }
133
134    /// Try to convert this builder into a [`PartialDeal`], validating that each
135    /// hand has at most 13 cards and the hands are pairwise disjoint.  On
136    /// failure the input is returned unchanged as the error.
137    ///
138    /// # Errors
139    ///
140    /// Returns `self` unchanged if the builder is not a valid subset.
141    pub fn build_partial(self) -> Result<PartialDeal, Self> {
142        let mut seen = Hand::EMPTY;
143        for hand in self.0 {
144            if hand.len() > 13 || hand & seen != Hand::EMPTY {
145                return Err(self);
146            }
147            seen |= hand;
148        }
149        Ok(PartialDeal(self))
150    }
151
152    /// Try to convert this builder into a [`FullDeal`], validating that each
153    /// hand has exactly 13 cards and the hands are pairwise disjoint.  On
154    /// failure the input is returned unchanged as the error.
155    ///
156    /// # Errors
157    ///
158    /// Returns `self` unchanged if the builder is not a valid full deal.
159    pub fn build_full(self) -> Result<FullDeal, Self> {
160        match self.build_partial() {
161            Ok(subset) if subset.len() == 52 => Ok(FullDeal(subset.0)),
162            Ok(subset) => Err(subset.0),
163            Err(builder) => Err(builder),
164        }
165    }
166}
167
168/// A validated subset of a bridge deal
169///
170/// Invariants: each hand holds at most 13 cards, and the four hands are
171/// pairwise disjoint.  Construct via [`Builder::build_partial`],
172/// [`TryFrom<Builder>`], the infallible widening from a [`FullDeal`], or by
173/// parsing a PBN-ish string.
174///
175/// `PartialDeal` is read-only: it exposes [`Index<Seat>`](ops::Index) but not
176/// [`IndexMut`](ops::IndexMut).  To mutate, widen back to a [`Builder`].
177///
178/// Parses the [PBN] deal format with relaxed per-hand size —
179/// `<dealer>:<hand> <hand> <hand> <hand>` — where each hand is four
180/// dot-separated holdings ordered spades, hearts, diamonds, clubs.  Holdings
181/// may be empty or contain `x` spot cards for unknown ranks.  Hands are
182/// listed clockwise starting from the dealer.
183///
184/// [PBN]: https://www.tistis.nl/pbn/
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
186#[cfg_attr(
187    feature = "serde",
188    derive(serde_with::SerializeDisplay, serde_with::DeserializeFromStr)
189)]
190pub struct PartialDeal(Builder);
191
192impl ops::Index<Seat> for PartialDeal {
193    type Output = Hand;
194
195    #[inline]
196    fn index(&self, seat: Seat) -> &Hand {
197        &self.0[seat]
198    }
199}
200
201impl PartialDeal {
202    /// Empty subset — all four hands empty
203    pub const EMPTY: Self = Self(Builder::new());
204
205    /// Collect all cards in the subset into a single hand
206    #[must_use]
207    pub fn collected(&self) -> Hand {
208        self.0.into_iter().fold(Hand::EMPTY, |a, h| a | h)
209    }
210
211    /// Total number of cards across the four hands
212    #[must_use]
213    pub fn len(&self) -> usize {
214        self.collected().len()
215    }
216
217    /// Whether the subset has no cards at all
218    #[must_use]
219    pub fn is_empty(&self) -> bool {
220        self.collected().is_empty()
221    }
222
223    /// PBN-compatible display from a seat's perspective
224    #[must_use]
225    pub fn display(&self, seat: Seat) -> impl fmt::Display + use<> {
226        DisplayAt {
227            builder: self.0,
228            seat,
229        }
230    }
231}
232
233impl From<PartialDeal> for Builder {
234    #[inline]
235    fn from(subset: PartialDeal) -> Self {
236        subset.0
237    }
238}
239
240impl TryFrom<Builder> for PartialDeal {
241    type Error = Builder;
242
243    #[inline]
244    fn try_from(builder: Builder) -> Result<Self, Self::Error> {
245        builder.build_partial()
246    }
247}
248
249impl FromStr for PartialDeal {
250    type Err = ParseDealError;
251
252    fn from_str(s: &str) -> Result<Self, Self::Err> {
253        parse_pbn(s)?
254            .build_partial()
255            .map_err(|_| ParseDealError::InvalidPartialDeal)
256    }
257}
258
259impl fmt::Display for PartialDeal {
260    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
261        self.display(Seat::North).fmt(f)
262    }
263}
264
265/// A full bridge deal — exactly 13 cards per hand, 52 total
266///
267/// Invariants: each of the four hands contains exactly 13 cards, and the
268/// hands partition the full 52-card deck.  Construct via
269/// [`Builder::build_full`], [`TryFrom<Builder>`], [`TryFrom<PartialDeal>`], or by
270/// parsing a PBN string.
271///
272/// `FullDeal` is read-only.  Parses the [PBN] deal format:
273/// `<dealer>:<hand> <hand> <hand> <hand>`, where each hand is four
274/// dot-separated holdings ordered spades, hearts, diamonds, clubs.  Hands
275/// are listed clockwise starting from the dealer.
276///
277/// # Examples
278///
279/// ```
280/// use dds_bridge::{FullDeal, Rank, Seat, Suit};
281///
282/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
283/// let deal: FullDeal = "N:.63.AKQ987.A9732 A8654.KQ5.T.QJT6 \
284///                       J973.J98742.3.K4 KQT2.AT.J6542.85".parse()?;
285/// assert!(deal[Seat::East][Suit::Spades].contains(Rank::A));
286/// # Ok(())
287/// # }
288/// ```
289///
290/// [PBN]: https://www.tistis.nl/pbn/
291#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
292#[cfg_attr(
293    feature = "serde",
294    derive(serde_with::SerializeDisplay, serde_with::DeserializeFromStr)
295)]
296pub struct FullDeal(Builder);
297
298impl ops::Index<Seat> for FullDeal {
299    type Output = Hand;
300
301    #[inline]
302    fn index(&self, seat: Seat) -> &Hand {
303        &self.0[seat]
304    }
305}
306
307impl IntoIterator for FullDeal {
308    type Item = Hand;
309    type IntoIter = core::array::IntoIter<Hand, 4>;
310
311    #[inline]
312    fn into_iter(self) -> Self::IntoIter {
313        self.0.into_iter()
314    }
315}
316
317impl FullDeal {
318    /// PBN-compatible display from a seat's perspective
319    #[must_use]
320    pub fn display(&self, seat: Seat) -> impl fmt::Display + use<> {
321        DisplayAt {
322            builder: self.0,
323            seat,
324        }
325    }
326}
327
328impl From<FullDeal> for Builder {
329    #[inline]
330    fn from(deal: FullDeal) -> Self {
331        deal.0
332    }
333}
334
335impl From<FullDeal> for PartialDeal {
336    #[inline]
337    fn from(deal: FullDeal) -> Self {
338        Self(deal.0)
339    }
340}
341
342impl TryFrom<Builder> for FullDeal {
343    type Error = Builder;
344
345    #[inline]
346    fn try_from(builder: Builder) -> Result<Self, Self::Error> {
347        builder.build_full()
348    }
349}
350
351impl TryFrom<PartialDeal> for FullDeal {
352    type Error = PartialDeal;
353
354    #[inline]
355    fn try_from(subset: PartialDeal) -> Result<Self, Self::Error> {
356        match subset.0.build_full() {
357            Ok(full) => Ok(full),
358            Err(builder) => Err(PartialDeal(builder)),
359        }
360    }
361}
362
363impl FromStr for FullDeal {
364    type Err = ParseDealError;
365
366    fn from_str(s: &str) -> Result<Self, Self::Err> {
367        parse_pbn(s)?
368            .build_full()
369            .map_err(|_| ParseDealError::NotFullDeal)
370    }
371}
372
373impl fmt::Display for FullDeal {
374    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
375        self.display(Seat::North).fmt(f)
376    }
377}
378
379/// Shared PBN deal parser: reads `<dealer>:<hand> <hand> <hand> <hand>` and
380/// returns a `Builder` with hands rotated so seat index 0 is North.
381fn parse_pbn(s: &str) -> Result<Builder, ParseDealError> {
382    let bytes = s.as_bytes();
383
384    let dealer = match bytes.first().map(u8::to_ascii_uppercase) {
385        Some(b'N') => Seat::North,
386        Some(b'E') => Seat::East,
387        Some(b'S') => Seat::South,
388        Some(b'W') => Seat::West,
389        _ => return Err(ParseDealError::InvalidDealer),
390    };
391
392    if bytes.get(1) != Some(&b':') {
393        return Err(ParseDealError::InvalidDealer);
394    }
395
396    let hands: Result<Vec<_>, _> = s[2..].split_whitespace().map(Hand::from_str).collect();
397
398    let mut builder = Builder(
399        hands?
400            .try_into()
401            .map_err(|_| ParseDealError::NotFourHands)?,
402    );
403    builder.0.rotate_right(dealer as usize);
404    Ok(builder)
405}
406
407/// Shared PBN-compatible `Display` helper for [`PartialDeal`] and [`FullDeal`]
408struct DisplayAt {
409    builder: Builder,
410    seat: Seat,
411}
412
413impl fmt::Display for DisplayAt {
414    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
415        f.write_char(self.seat.letter())?;
416        f.write_char(':')?;
417
418        self.builder[self.seat].fmt(f)?;
419        f.write_char(' ')?;
420
421        self.builder[self.seat.lho()].fmt(f)?;
422        f.write_char(' ')?;
423
424        self.builder[self.seat.partner()].fmt(f)?;
425        f.write_char(' ')?;
426
427        self.builder[self.seat.rho()].fmt(f)
428    }
429}