dds_bridge/deal/
mod.rs

1#[cfg(test)]
2mod test;
3
4use crate::contract::Strain;
5use core::fmt::{self, Write as _};
6use core::num::{NonZeroU8, Wrapping};
7use core::ops::{Add, AddAssign, BitAnd, BitOr, BitXor, Index, IndexMut, Not, Sub, SubAssign};
8use core::str::FromStr;
9use once_cell::sync::Lazy;
10use rand::prelude::SliceRandom as _;
11use thiserror::Error;
12
13/// A suit of playing cards
14///
15/// Suits are convertible to [`Strain`]s since suits form a subset of strains.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
17#[repr(u8)]
18pub enum Suit {
19    /// ♣, convertible to [`Strain::Clubs`]
20    Clubs,
21    /// ♦, convertible to [`Strain::Diamonds`]
22    Diamonds,
23    /// ♥, convertible to [`Strain::Hearts`]
24    Hearts,
25    /// ♠, convertible to [`Strain::Spades`]
26    Spades,
27}
28
29impl Suit {
30    /// Suits in the ascending order, the order in this crate
31    pub const ASC: [Self; 4] = [Self::Clubs, Self::Diamonds, Self::Hearts, Self::Spades];
32
33    /// Suits in the descending order, the order in [`dds_bridge_sys`]
34    pub const DESC: [Self; 4] = [Self::Spades, Self::Hearts, Self::Diamonds, Self::Clubs];
35}
36
37impl From<Suit> for Strain {
38    #[inline]
39    fn from(suit: Suit) -> Self {
40        match suit {
41            Suit::Clubs => Self::Clubs,
42            Suit::Diamonds => Self::Diamonds,
43            Suit::Hearts => Self::Hearts,
44            Suit::Spades => Self::Spades,
45        }
46    }
47}
48
49/// Error raised when converting [`Strain::Notrump`] to a suit
50#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
51#[error("Notrump is not a suit")]
52pub struct SuitFromNotrumpError;
53
54impl TryFrom<Strain> for Suit {
55    type Error = SuitFromNotrumpError;
56
57    #[inline]
58    fn try_from(strain: Strain) -> Result<Self, Self::Error> {
59        match strain {
60            Strain::Clubs => Ok(Self::Clubs),
61            Strain::Diamonds => Ok(Self::Diamonds),
62            Strain::Hearts => Ok(Self::Hearts),
63            Strain::Spades => Ok(Self::Spades),
64            Strain::Notrump => Err(SuitFromNotrumpError),
65        }
66    }
67}
68
69/// Position at the table
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
71#[repr(u8)]
72pub enum Seat {
73    /// Dealer of Board 1, partner of [`Seat::South`]
74    North,
75    /// Dealer of Board 2, partner of [`Seat::West`]
76    East,
77    /// Dealer of Board 3, partner of [`Seat::North`]
78    South,
79    /// Dealer of Board 4, partner of [`Seat::East`]
80    West,
81}
82
83impl Seat {
84    /// Seats in the order of dealing
85    pub const ALL: [Self; 4] = [Self::North, Self::East, Self::South, Self::West];
86}
87
88impl Add<Wrapping<u8>> for Seat {
89    type Output = Self;
90
91    #[inline]
92    fn add(self, rhs: Wrapping<u8>) -> Self {
93        // SAFETY: this is just modular arithmetics on a 4-element enum
94        unsafe { core::mem::transmute((Wrapping(self as u8) + rhs).0 & 3) }
95    }
96}
97
98impl Add<Seat> for Wrapping<u8> {
99    type Output = Seat;
100
101    #[inline]
102    fn add(self, rhs: Seat) -> Seat {
103        rhs + self
104    }
105}
106
107impl AddAssign<Wrapping<u8>> for Seat {
108    #[inline]
109    fn add_assign(&mut self, rhs: Wrapping<u8>) {
110        *self = *self + rhs;
111    }
112}
113
114impl Sub<Wrapping<u8>> for Seat {
115    type Output = Self;
116
117    #[inline]
118    fn sub(self, rhs: Wrapping<u8>) -> Self {
119        // SAFETY: this is just modular arithmetics on a 4-element enum
120        unsafe { core::mem::transmute((Wrapping(self as u8) - rhs).0 & 3) }
121    }
122}
123
124impl SubAssign<Wrapping<u8>> for Seat {
125    #[inline]
126    fn sub_assign(&mut self, rhs: Wrapping<u8>) {
127        *self = *self - rhs;
128    }
129}
130
131impl Sub<Self> for Seat {
132    type Output = Wrapping<u8>;
133
134    #[inline]
135    fn sub(self, rhs: Self) -> Wrapping<u8> {
136        (Wrapping(self as u8) - Wrapping(rhs as u8)) & Wrapping(3)
137    }
138}
139
140impl From<Seat> for char {
141    #[inline]
142    fn from(seat: Seat) -> Self {
143        match seat {
144            Seat::North => 'N',
145            Seat::East => 'E',
146            Seat::South => 'S',
147            Seat::West => 'W',
148        }
149    }
150}
151
152bitflags::bitflags! {
153    /// A set of seats
154    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
155    pub struct SeatFlags: u8 {
156        /// The set containing [`Seat::North`]
157        const NORTH = 0b0001;
158        /// The set containing [`Seat::East`]
159        const EAST = 0b0010;
160        /// The set containing [`Seat::South`]
161        const SOUTH = 0b0100;
162        /// The set containing [`Seat::West`]
163        const WEST = 0b1000;
164    }
165}
166
167impl SeatFlags {
168    /// The empty set
169    pub const EMPTY: Self = Self::empty();
170
171    /// The set containing all seats
172    pub const ALL: Self = Self::all();
173
174    /// The set containing [`Seat::North`] and [`Seat::South`]
175    pub const NS: Self = Self::NORTH.union(Self::SOUTH);
176
177    /// The set containing [`Seat::East`] and [`Seat::West`]
178    pub const EW: Self = Self::EAST.union(Self::WEST);
179}
180
181impl From<Seat> for SeatFlags {
182    #[inline]
183    fn from(seat: Seat) -> Self {
184        match seat {
185            Seat::North => Self::NORTH,
186            Seat::East => Self::EAST,
187            Seat::South => Self::SOUTH,
188            Seat::West => Self::WEST,
189        }
190    }
191}
192
193/// A playing card
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
195pub struct Card(NonZeroU8);
196
197impl Card {
198    /// Create a card from suit and rank
199    ///
200    /// The rank is a number from 2 to 14.  J, Q, K, A are encoded as 11, 12,
201    /// 13, 14 respectively.
202    ///
203    /// # Panics
204    /// Panics if the rank is not in the range 2..=14.
205    #[must_use]
206    #[inline]
207    pub const fn new(suit: Suit, rank: u8) -> Self {
208        assert!(rank >= 2 && rank <= 14);
209        // SAFETY: rank is guaranteed to be non-zero
210        Self(unsafe { NonZeroU8::new_unchecked(rank << 2 | suit as u8) })
211    }
212
213    /// The suit of the card
214    #[must_use]
215    #[inline]
216    pub const fn suit(self) -> Suit {
217        // SAFETY: suit is guaranteed to be valid, in (0..=3)
218        unsafe { core::mem::transmute(self.0.get() & 3) }
219    }
220
221    /// The rank of the card
222    ///
223    /// The rank is a number from 2 to 14.  J, Q, K, A are denoted as 11, 12,
224    /// 13, 14 respectively.
225    #[must_use]
226    #[inline]
227    pub const fn rank(self) -> u8 {
228        self.0.get() >> 2
229    }
230}
231
232/// A bitset whose size is known at compile time
233pub trait SmallSet<T>: Copy + Eq + BitAnd + BitOr + BitXor + Not + Sub {
234    /// The empty set
235    const EMPTY: Self;
236
237    /// The set containing all possible values
238    const ALL: Self;
239
240    /// The number of elements in the set
241    #[must_use]
242    fn len(self) -> usize;
243
244    /// Whether the set is empty
245    #[must_use]
246    #[inline]
247    fn is_empty(self) -> bool {
248        self == Self::EMPTY
249    }
250
251    /// Whether the set contains a value
252    fn contains(self, value: T) -> bool;
253
254    /// Insert a value into the set
255    fn insert(&mut self, value: T) -> bool;
256
257    /// Remove a value from the set
258    fn remove(&mut self, value: T) -> bool;
259
260    /// Toggle a value in the set
261    fn toggle(&mut self, value: T) -> bool;
262
263    /// Conditionally insert/remove a value from the set
264    fn set(&mut self, value: T, condition: bool) {
265        if condition {
266            self.insert(value);
267        } else {
268            self.remove(value);
269        }
270    }
271
272    /// Iterate over the values in the set
273    fn iter(self) -> impl Iterator<Item = T>;
274
275    /// Intersection of two sets
276    #[must_use]
277    #[inline]
278    fn intersection(self, rhs: Self) -> <Self as BitAnd>::Output {
279        self & rhs
280    }
281
282    /// Union of two sets
283    #[must_use]
284    #[inline]
285    fn union(self, rhs: Self) -> <Self as BitOr>::Output {
286        self | rhs
287    }
288
289    /// Difference of two sets
290    #[must_use]
291    #[inline]
292    fn difference(self, rhs: Self) -> <Self as Sub>::Output {
293        self - rhs
294    }
295
296    /// Symmetric difference of two sets
297    #[must_use]
298    #[inline]
299    fn symmetric_difference(self, rhs: Self) -> <Self as BitXor>::Output {
300        self ^ rhs
301    }
302
303    /// Complement of the set
304    #[must_use]
305    #[inline]
306    fn complement(self) -> <Self as Not>::Output {
307        !self
308    }
309}
310
311/// A set of cards of the same suit
312#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
313pub struct Holding(u16);
314
315#[derive(Debug, Clone, PartialEq, Eq)]
316struct HoldingIter {
317    rest: u16,
318    cursor: u8,
319}
320
321impl Iterator for HoldingIter {
322    type Item = u8;
323
324    #[inline]
325    fn next(&mut self) -> Option<Self::Item> {
326        if self.rest == 0 {
327            return None;
328        }
329        // 1. Trailing zeros are in the range of 0..=15, which fits in `u8``
330        // 2. Trailing zeros cannot be 15 since the bitset is from a `Holding`
331        #[allow(clippy::cast_possible_truncation)]
332        let step = self.rest.trailing_zeros() as u8 + 1;
333        self.rest >>= step;
334        self.cursor += step;
335        Some(self.cursor - 1)
336    }
337
338    #[inline]
339    fn size_hint(&self) -> (usize, Option<usize>) {
340        let count = self.rest.count_ones() as usize;
341        (count, Some(count))
342    }
343
344    #[inline]
345    fn count(self) -> usize {
346        self.rest.count_ones() as usize
347    }
348}
349
350impl SmallSet<u8> for Holding {
351    const EMPTY: Self = Self(0);
352    const ALL: Self = Self(0x7FFC);
353
354    #[inline]
355    fn len(self) -> usize {
356        self.0.count_ones() as usize
357    }
358
359    #[inline]
360    fn contains(self, rank: u8) -> bool {
361        self.0 & 1 << rank != 0
362    }
363
364    #[inline]
365    fn insert(&mut self, rank: u8) -> bool {
366        let insertion = 1 << rank & Self::ALL.0;
367        let inserted = insertion & !self.0 != 0;
368        self.0 |= insertion;
369        inserted
370    }
371
372    #[inline]
373    fn remove(&mut self, rank: u8) -> bool {
374        let removed = self.contains(rank);
375        self.0 &= !(1 << rank);
376        removed
377    }
378
379    #[inline]
380    fn toggle(&mut self, rank: u8) -> bool {
381        self.0 ^= 1 << rank & Self::ALL.0;
382        self.contains(rank)
383    }
384
385    #[inline]
386    fn set(&mut self, rank: u8, condition: bool) {
387        let flag = 1 << rank;
388        let mask = u16::from(condition).wrapping_neg();
389        self.0 = (self.0 & !flag) | (mask & flag);
390    }
391
392    #[inline]
393    fn iter(self) -> impl Iterator<Item = u8> {
394        HoldingIter {
395            rest: self.0,
396            cursor: 0,
397        }
398    }
399}
400
401impl Holding {
402    /// As a bitset of ranks
403    #[must_use]
404    #[inline]
405    pub const fn to_bits(self) -> u16 {
406        self.0
407    }
408
409    /// Create a holding from a bitset of ranks, retaining invalid ranks
410    #[must_use]
411    #[inline]
412    pub const fn from_bits_retain(bits: u16) -> Self {
413        Self(bits)
414    }
415
416    /// Whether the holding contains an invalid rank
417    #[must_use]
418    #[inline]
419    pub const fn contains_unknown_bits(self) -> bool {
420        self.0 & Self::ALL.0 != self.0
421    }
422
423    /// Create a holding from a bitset of ranks, checking for invalid ranks
424    #[must_use]
425    #[inline]
426    pub const fn from_bits(bits: u16) -> Option<Self> {
427        if bits & Self::ALL.0 == bits {
428            Some(Self(bits))
429        } else {
430            None
431        }
432    }
433
434    /// Create a holding from a bitset of ranks, removing invalid ranks
435    #[must_use]
436    #[inline]
437    pub const fn from_bits_truncate(bits: u16) -> Self {
438        Self(bits & Self::ALL.0)
439    }
440
441    /// Create a holding from a rank
442    #[must_use]
443    #[inline]
444    pub const fn from_rank(rank: u8) -> Self {
445        Self(1 << rank)
446    }
447}
448
449impl BitAnd for Holding {
450    type Output = Self;
451
452    #[inline]
453    fn bitand(self, rhs: Self) -> Self {
454        Self(self.0 & rhs.0)
455    }
456}
457
458impl BitOr for Holding {
459    type Output = Self;
460
461    #[inline]
462    fn bitor(self, rhs: Self) -> Self {
463        Self(self.0 | rhs.0)
464    }
465}
466
467impl BitXor for Holding {
468    type Output = Self;
469
470    #[inline]
471    fn bitxor(self, rhs: Self) -> Self {
472        Self(self.0 ^ rhs.0)
473    }
474}
475
476impl Not for Holding {
477    type Output = Self;
478
479    #[inline]
480    fn not(self) -> Self {
481        Self::from_bits_truncate(!self.0)
482    }
483}
484
485impl Sub for Holding {
486    type Output = Self;
487
488    #[inline]
489    fn sub(self, rhs: Self) -> Self {
490        Self(self.0 & !rhs.0)
491    }
492}
493
494/// Show cards in descending order
495///
496/// 1. The ten is shown as `T` for PBN compatibility.
497/// 2. This implementation ignores formatting flags for simplicity and speed.
498///    If you want to pad or align the output, use [`fmt::Formatter::pad`].
499impl fmt::Display for Holding {
500    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
501        for rank in (2..15).rev() {
502            if self.contains(rank) {
503                f.write_char(b"23456789TJQKA"[rank as usize - 2] as char)?;
504            }
505        }
506        Ok(())
507    }
508}
509
510/// An error which can be returned when parsing a [`Holding`], a [`Hand`], or a [`Deal`]
511#[derive(Debug, Error, Clone, Copy, PartialEq, Eq, Hash)]
512pub enum ParseHandError {
513    /// Ranks are not all valid or in descending order
514    #[error("Ranks are not all valid or in descending order")]
515    InvalidHolding,
516
517    /// The same rank appears more than once
518    #[error("The same rank appears more than once")]
519    RepeatedRank,
520
521    /// A suit contains more than 13 cards
522    #[error("A suit contains more than 13 cards")]
523    TooManyCards,
524
525    /// The hand does not contain 4 suits
526    #[error("The hand does not contain 4 suits")]
527    NotFourSuits,
528
529    /// Invalid dealer tag for a deal
530    #[error("Invalid dealer tag for a deal")]
531    InvalidDealer,
532
533    /// The deal does not contain 4 hands
534    #[error("The deal does not contain 4 hands")]
535    NotFourHands,
536}
537
538impl FromStr for Holding {
539    type Err = ParseHandError;
540
541    fn from_str(s: &str) -> Result<Self, Self::Err> {
542        static RE: Lazy<regex::Regex> = Lazy::new(|| {
543            regex::RegexBuilder::new("^(A?K?Q?J?(?:T|10)?9?8?7?6?5?4?3?2?)(x*)$")
544                .case_insensitive(true)
545                .build()
546                .expect("Invalid regex")
547        });
548
549        // 13 cards + 1 ten
550        if s.len() > 13 + 1 {
551            return Err(ParseHandError::TooManyCards);
552        }
553
554        let Some((_, [explicit, spots])) = RE.captures(s).map(|x| x.extract()) else {
555            return Err(ParseHandError::InvalidHolding);
556        };
557
558        let explicit = explicit.bytes().try_fold(Self::EMPTY, |mut holding, c| {
559            let rank = match c.to_ascii_uppercase() {
560                b'1' => return Ok(holding),
561                b'2' => 2,
562                b'3' => 3,
563                b'4' => 4,
564                b'5' => 5,
565                b'6' => 6,
566                b'7' => 7,
567                b'8' => 8,
568                b'9' => 9,
569                b'T' | b'0' => 10,
570                b'J' => 11,
571                b'Q' => 12,
572                b'K' => 13,
573                b'A' => 14,
574                _ => unreachable!("Invalid ranks should have been caught by the regex"),
575            };
576
577            if holding.insert(rank) {
578                Ok(holding)
579            } else {
580                Err(ParseHandError::RepeatedRank)
581            }
582        })?;
583
584        let spots = Self::from_bits_truncate((4 << spots.len()) - 4);
585
586        if explicit & spots == Self::EMPTY {
587            Ok(explicit | spots)
588        } else {
589            Err(ParseHandError::RepeatedRank)
590        }
591    }
592}
593
594/// A hand of playing cards
595#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
596pub struct Hand(pub [Holding; 4]);
597
598impl Index<Suit> for Hand {
599    type Output = Holding;
600
601    #[inline]
602    fn index(&self, suit: Suit) -> &Holding {
603        &self.0[suit as usize]
604    }
605}
606
607impl IndexMut<Suit> for Hand {
608    #[inline]
609    fn index_mut(&mut self, suit: Suit) -> &mut Holding {
610        &mut self.0[suit as usize]
611    }
612}
613
614impl Hand {
615    /// As a bitset of cards
616    #[must_use]
617    #[inline]
618    pub const fn to_bits(self) -> u64 {
619        // SAFETY: every combination of 64 bits is a valid `u64`
620        unsafe { core::mem::transmute(self.0) }
621    }
622
623    /// Create a hand from a bitset of cards, retaining invalid cards
624    #[must_use]
625    #[inline]
626    pub const fn from_bits_retain(bits: u64) -> Self {
627        // SAFETY: safe as long as the transmutation holds
628        unsafe { core::mem::transmute(bits) }
629    }
630
631    /// Whether the hand contains an invalid card
632    #[must_use]
633    #[inline]
634    pub const fn contains_unknown_bits(self) -> bool {
635        self.to_bits() & Self::ALL.to_bits() != self.to_bits()
636    }
637
638    /// Create a hand from a bitset of cards, checking for invalid cards
639    #[must_use]
640    #[inline]
641    pub const fn from_bits(bits: u64) -> Option<Self> {
642        if bits & Self::ALL.to_bits() == bits {
643            Some(Self::from_bits_retain(bits))
644        } else {
645            None
646        }
647    }
648
649    /// Create a hand from a bitset of cards, removing invalid cards
650    #[must_use]
651    #[inline]
652    pub const fn from_bits_truncate(bits: u64) -> Self {
653        Self::from_bits_retain(bits & Self::ALL.to_bits())
654    }
655}
656
657impl SmallSet<Card> for Hand {
658    const EMPTY: Self = Self([Holding::EMPTY; 4]);
659    const ALL: Self = Self([Holding::ALL; 4]);
660
661    #[inline]
662    fn len(self) -> usize {
663        self.to_bits().count_ones() as usize
664    }
665
666    #[inline]
667    fn contains(self, card: Card) -> bool {
668        self[card.suit()].contains(card.rank())
669    }
670
671    #[inline]
672    fn insert(&mut self, card: Card) -> bool {
673        self[card.suit()].insert(card.rank())
674    }
675
676    #[inline]
677    fn remove(&mut self, card: Card) -> bool {
678        self[card.suit()].remove(card.rank())
679    }
680
681    #[inline]
682    fn toggle(&mut self, card: Card) -> bool {
683        self[card.suit()].toggle(card.rank())
684    }
685
686    #[inline]
687    fn set(&mut self, card: Card, condition: bool) {
688        self[card.suit()].set(card.rank(), condition);
689    }
690
691    #[inline]
692    fn iter(self) -> impl Iterator<Item = Card> {
693        Suit::ASC
694            .into_iter()
695            .flat_map(move |suit| self[suit].iter().map(move |rank| Card::new(suit, rank)))
696    }
697}
698
699/// PBN-compatible display of a hand
700///
701/// This implementation ignores formatting flags for simplicity and speed.
702impl fmt::Display for Hand {
703    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
704        if self.is_empty() {
705            return f.write_char('-');
706        }
707
708        self[Suit::Spades].fmt(f)?;
709        f.write_char('.')?;
710
711        self[Suit::Hearts].fmt(f)?;
712        f.write_char('.')?;
713
714        self[Suit::Diamonds].fmt(f)?;
715        f.write_char('.')?;
716
717        self[Suit::Clubs].fmt(f)
718    }
719}
720
721impl FromStr for Hand {
722    type Err = ParseHandError;
723
724    fn from_str(s: &str) -> Result<Self, Self::Err> {
725        // 52 cards + 4 tens + 3 dots
726        if s.len() > 52 + 4 + 3 {
727            return Err(ParseHandError::TooManyCards);
728        }
729
730        if s == "-" {
731            return Ok(Self::EMPTY);
732        }
733
734        let holdings: Result<Vec<_>, _> = s.split('.').map(Holding::from_str).rev().collect();
735
736        Ok(Self(
737            holdings?
738                .try_into()
739                .map_err(|_| ParseHandError::NotFourSuits)?,
740        ))
741    }
742}
743
744impl BitAnd for Hand {
745    type Output = Self;
746
747    #[inline]
748    fn bitand(self, rhs: Self) -> Self {
749        Self::from_bits_retain(self.to_bits() & rhs.to_bits())
750    }
751}
752
753impl BitOr for Hand {
754    type Output = Self;
755
756    #[inline]
757    fn bitor(self, rhs: Self) -> Self {
758        Self::from_bits_retain(self.to_bits() | rhs.to_bits())
759    }
760}
761
762impl BitXor for Hand {
763    type Output = Self;
764
765    #[inline]
766    fn bitxor(self, rhs: Self) -> Self {
767        Self::from_bits_retain(self.to_bits() ^ rhs.to_bits())
768    }
769}
770
771impl Not for Hand {
772    type Output = Self;
773
774    #[inline]
775    fn not(self) -> Self {
776        Self::from_bits_truncate(!self.to_bits())
777    }
778}
779
780impl Sub for Hand {
781    type Output = Self;
782
783    #[inline]
784    fn sub(self, rhs: Self) -> Self {
785        Self::from_bits_retain(self.to_bits() & !rhs.to_bits())
786    }
787}
788
789/// A deal of four hands
790#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
791pub struct Deal(pub [Hand; 4]);
792
793impl Index<Seat> for Deal {
794    type Output = Hand;
795
796    #[inline]
797    fn index(&self, seat: Seat) -> &Hand {
798        &self.0[seat as usize]
799    }
800}
801
802impl IndexMut<Seat> for Deal {
803    #[inline]
804    fn index_mut(&mut self, seat: Seat) -> &mut Hand {
805        &mut self.0[seat as usize]
806    }
807}
808
809struct DealDisplay {
810    deal: Deal,
811    seat: Seat,
812}
813
814impl fmt::Display for DealDisplay {
815    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
816        f.write_char(char::from(self.seat))?;
817        f.write_char(':')?;
818
819        self.deal[self.seat].fmt(f)?;
820        f.write_char(' ')?;
821
822        self.deal[self.seat + Wrapping(1)].fmt(f)?;
823        f.write_char(' ')?;
824
825        self.deal[self.seat + Wrapping(2)].fmt(f)?;
826        f.write_char(' ')?;
827
828        self.deal[self.seat + Wrapping(3)].fmt(f)
829    }
830}
831
832impl Deal {
833    /// Create a deal from a shuffled standard 52-card deck
834    pub fn new(rng: &mut (impl rand::Rng + ?Sized)) -> Self {
835        let mut deck: [_; 52] = core::array::from_fn(|i| {
836            // Safe because `i` is always in the range of 0..52
837            #[allow(clippy::cast_possible_truncation)]
838            let i = i as u8;
839            let suit: Suit = unsafe { core::mem::transmute(i & 3) };
840            Card::new(suit, (i >> 2) + 2)
841        });
842        deck.shuffle(rng);
843
844        deck.into_iter()
845            .enumerate()
846            .fold(Self::default(), |mut deal, (i, card)| {
847                // SAFETY: `i & 3` is always in the range of 0..=3
848                #[allow(clippy::cast_possible_truncation)]
849                let seat: Seat = unsafe { core::mem::transmute((i & 3) as u8) };
850                deal[seat].insert(card);
851                deal
852            })
853    }
854
855    /// PBN-compatible display from a seat's perspective
856    #[must_use]
857    #[inline]
858    pub fn display(self, seat: Seat) -> impl fmt::Display {
859        DealDisplay { deal: self, seat }
860    }
861
862    /// Shuffle existing hands while preserving the numbers of cards
863    #[must_use]
864    pub fn shuffled(self, rng: &mut (impl rand::Rng + ?Sized), seats: SeatFlags) -> Self {
865        let mut deal = Self::default();
866        let mut deck = Vec::with_capacity(52);
867        let mut lengths = [0; 4];
868
869        for seat in Seat::ALL {
870            if seats.contains(seat.into()) {
871                deck.extend(self[seat].iter());
872                lengths[seat as usize] = self[seat].len();
873            } else {
874                deal[seat] = self[seat];
875            }
876        }
877
878        deck.shuffle(rng);
879        let mut seat = Seat::North;
880
881        loop {
882            let Some(card) = deck.pop() else { break };
883
884            while lengths[seat as usize] == 0 {
885                seat += Wrapping(1);
886            }
887
888            lengths[seat as usize] -= 1;
889            deal[seat].insert(card);
890        }
891        deal
892    }
893}
894
895impl FromStr for Deal {
896    type Err = ParseHandError;
897
898    fn from_str(s: &str) -> Result<Self, Self::Err> {
899        static DEALER: Lazy<regex::Regex> = Lazy::new(|| {
900            regex::RegexBuilder::new(r"^[NESW]:\s*")
901                .case_insensitive(true)
902                .build()
903                .expect("Invalid regex")
904        });
905
906        let Some(tag) = DEALER.find(s) else {
907            return Err(ParseHandError::InvalidDealer);
908        };
909
910        let dealer = match s.as_bytes()[0].to_ascii_uppercase() {
911            b'N' => Seat::North,
912            b'E' => Seat::East,
913            b'S' => Seat::South,
914            b'W' => Seat::West,
915            _ => unreachable!("Invalid dealer should have been caught by the regex"),
916        };
917
918        let hands: Result<Vec<_>, _> = s[tag.end()..]
919            .split_whitespace()
920            .map(Hand::from_str)
921            .collect();
922
923        let mut deal = Self(
924            hands?
925                .try_into()
926                .map_err(|_| ParseHandError::NotFourHands)?,
927        );
928        deal.0.rotate_right(dealer as usize);
929        Ok(deal)
930    }
931}