playin_cards/lib.rs
1#![cfg_attr(docsrs, feature(doc_auto_cfg))]
2#![doc = include_str!("../README.md")]
3//! # Usage
4//! The main interface is the [`Card`] which represents a single playing card
5//! which may be one of the cards in a standard 52-card deck of playing cards,
6//! or a Joker which may be either black or red. A [`Card`] instance can be
7//! easily be parsed from a 2-`char` `str` with one of `A23456789TJQK` as the
8//! first char and one of `SsCcDdHh♥♣♠♦` as the second char.
9//!
10//! `Card` can also be formatted as a 2-`char` unicode string such `A♣`, `5♥`,
11//! `J♠` with the [`Display`] trait or as a sinlg-`char` unicode card char such
12//! as `🃑`, `🂵`, `🂫` with the [`UpperHex`] trait.
13
14#[allow(unused_imports)]
15use std::fmt::{Display, UpperHex};
16#[cfg(feature = "rank-partialord")]
17use std::cmp::{PartialOrd, Ordering};
18
19use itertools::Itertools;
20use strum_macros::EnumIter;
21use strum::IntoEnumIterator;
22
23/// number of cards in a standard 52-card deck of French-suited playing cards
24pub const CARDS_PER_DECK: u8 = 52;
25
26/// A French-suited playing card
27///
28/// The card is represented by either `Joker`, which has only a `Color` or a
29/// `Regular` which represents a normal card from a standard 52-card deck,
30/// comprised of a [`Rank`] and a [`Suit`]
31///
32/// ## Parsing
33///
34/// `Card` can easily be parsed from a 2-`char` `str` representation where the
35/// first char represents the [`Rank`] as one of `A23456789TJQK`) and the
36/// second char represents the [`Suit`] as one of `SsCcDdHh♠♥♣♦`
37/// ```
38/// use playin_cards::{Card::*, Rank::*, Suit::*};
39///
40/// // Parse a Card instance from a 2-char ASCII string:
41/// assert_eq!("AS".parse(), Ok(Regular{rank: Ace, suit: Spades}));
42/// assert_eq!("3D".parse(), Ok(Regular{rank: Three, suit: Diamonds}));
43/// assert_eq!("JC".parse(), Ok(Regular{rank: Jack, suit: Clubs}));
44/// assert_eq!("9H".parse(), Ok(Regular{rank: Nine, suit: Hearts}));
45///
46/// // A Card can also be parsed from Unicode representation:
47/// assert_eq!("A♣".parse(), Ok(Regular{rank: Ace, suit: Clubs}));
48/// assert_eq!("5♠".parse(), Ok(Regular{rank: Five, suit: Spades}));
49/// assert_eq!("8♥".parse(), Ok(Regular{rank: Eight, suit: Hearts}));
50/// ```
51///
52/// ## Formatting
53/// `Card` can be formatted as a 2-`char` Unicode `str` using [`Display`] or as
54/// a single-`char` unicode `str` using [`UpperHex`]
55///
56/// ```
57/// use playin_cards::{Card::*, Rank::*, Suit::*};
58///
59/// // `Display` uses ASCII for rank and Unicode for Suit:
60/// assert_eq!(format!("{}", Regular{rank: Ace, suit: Clubs}), "A♣");
61/// assert_eq!(format!("{}", Regular{rank: King, suit: Diamonds}), "K♦");
62/// assert_eq!(format!("{}", Regular{rank: Ace, suit: Clubs}), "A♣");
63///
64/// // `UpperHex` uses Unicode card chars
65/// assert_eq!(format!("{:X}", Regular{rank: Ace, suit: Clubs}), "🃑");
66/// ```
67#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
68pub enum Card {
69 /// Regular playing card from a standard 52-card deck.
70 Regular { rank: Rank, suit: Suit },
71 /// A Joker
72 Joker(Color),
73}
74
75/// Ranks of a standard French-suited playing card
76///
77/// ## Values
78/// `Rank` instances do not have a notion of "value". Value is not an intrinsic
79/// property of the rank of a playing card, rather it is a property imposed on
80/// it by the rank valuing system of the specific game in which the card is
81/// being used; for example one game might assign a value of 11 to `Ace` while
82/// another may assign it a value of 1, and others may allow the player to
83/// decide if it is 1 or 11 based on other cards in their hand. For this reason
84/// it is inappropriate for this library to expose a public interface to the
85/// concept of value for `Rank`, it is up to this library's consumers to define
86/// how values are assigned to ranks, if at all.
87///
88/// That said, the discriminants of the variants of `Rank` start at 1 for Ace
89/// and increase by 1 all the way up to `King`, as this facilitates the
90/// implementations of what is by-far the most common rank valuing system used
91/// in card games.
92/// ```
93/// use playin_cards::Rank;
94/// assert_eq!(Rank::Ace as u8, 1);
95/// assert_eq!(Rank::Six as u8, 6);
96/// assert_eq!(Rank::King as u8, 13);
97/// ```
98///
99/// ## Conversions
100///
101/// `Rank` is bidirectionally convertible with `char` via `From` and `TryFrom`:
102///
103/// ```
104/// use playin_cards::{Rank, ParseError};
105///
106/// // get the char corresponding to a `Rank` instance
107/// assert_eq!(char::from(Rank::King), 'K');
108/// assert_eq!(char::from(Rank::Five), '5');
109/// assert_eq!(char::from(Rank::Jack), 'J');
110///
111/// // parse a single `char` to a `Rank`
112/// assert_eq!(Rank::try_from('Q'), Ok(Rank::Queen));
113/// assert_eq!(Rank::try_from('4'), Ok(Rank::Four));
114/// assert_eq!(Rank::try_from('T'), Ok(Rank::Ten));
115/// assert_eq!(Rank::try_from('A'), Ok(Rank::Ace));
116///
117/// // Parsing fails if an invalid char is used
118/// assert_eq!(Rank::try_from('Y'), Err(ParseError::InvalidChar));
119/// ```
120///
121/// ## Iteration
122/// Thanks to [`strum`], the vairants of [`Rank`] can be iterated over:
123/// ```
124/// use playin_cards::Rank;
125/// use strum::IntoEnumIterator;
126///
127/// let mut rankiter = Rank::iter();
128/// assert_eq!(rankiter.next(), Some(Rank::Ace));
129/// assert_eq!(rankiter.next(), Some(Rank::Two));
130/// assert_eq!(rankiter.next(), Some(Rank::Three));
131/// assert_eq!(rankiter.next(), Some(Rank::Four));
132/// // You get the point...
133///
134/// ```
135#[derive(EnumIter, Debug, PartialEq, Clone, Copy, Eq, Hash)]
136pub enum Rank {
137 Ace = 1,
138 Two,
139 Three,
140 Four,
141 Five,
142 Six,
143 Seven,
144 Eight,
145 Nine,
146 Ten,
147 Jack,
148 Queen,
149 King,
150}
151
152#[cfg(feature = "rank-partialord")]
153impl PartialOrd for Rank {
154 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
155 PartialOrd::partial_cmp(self as u8, other as u8)
156 }
157}
158
159/// Suits of a standard French-suited playing card
160///
161/// ## Conversions
162/// `Suit` is bidirectionally convertible with `char`:
163/// ```
164/// use playin_cards::Suit;
165/// // Converting Suit -> char uses Unicode
166/// assert_eq!(char::from(Suit::Spades), '♠');
167/// assert_eq!(char::from(Suit::Hearts), '♥');
168/// assert_eq!(char::from(Suit::Diamonds), '♦');
169/// assert_eq!(char::from(Suit::Clubs), '♣');
170/// // Converting char -> Suit allows uppercase ASCII, lowercase ASCII, or
171/// // Unicode
172/// assert_eq!(Suit::try_from('S'), Ok(Suit::Spades)); // Uppercase
173/// assert_eq!(Suit::try_from('s'), Ok(Suit::Spades)); // Lowercase
174/// assert_eq!(Suit::try_from('♠'), Ok(Suit::Spades)); // Unicode
175/// ```
176/// ## Iteration
177/// Thanks to [`strum`], the variants of [`Suit`] can be iterated over:
178/// ```
179/// use playin_cards::Suit;
180/// use strum::IntoEnumIterator;
181///
182/// let mut suititer = Suit::iter();
183/// assert_eq!(suititer.next(), Some(Suit::Hearts));
184/// assert_eq!(suititer.next(), Some(Suit::Diamonds));
185/// assert_eq!(suititer.next(), Some(Suit::Spades));
186/// assert_eq!(suititer.next(), Some(Suit::Clubs));
187///
188/// ```
189#[derive(EnumIter, Debug, Clone, Copy, PartialEq, Eq, Hash)]
190pub enum Suit {
191 Hearts,
192 Diamonds,
193 Spades,
194 Clubs,
195}
196
197/// Color of a [`Suit`] or [`Card`]. Either Red or Black
198#[derive(Debug, PartialEq, Clone, Copy, Eq, Hash)]
199pub enum Color {
200 Red,
201 Black,
202}
203
204mod format;
205mod parse;
206
207pub use parse::ParseError;
208
209impl Suit {
210 /// Set of allowed chars which can be converted to [`Suit`] with
211 /// [`TryFrom<char>`]
212 pub const ALLOWED_CHARS: &'static str = "SsCcHhDd♠♣♥♦";
213
214 /// Return the color of the `Suit`
215 pub fn color(self) -> Color {
216 match self {
217 Suit::Hearts | Suit::Diamonds => Color::Red,
218 Suit::Spades | Suit::Clubs => Color::Black,
219 }
220 }
221}
222
223impl Card {
224 /// Return the color of the `Card` which is just the color of its `Suit`
225 pub fn color(self) -> Color {
226 match self {
227 Self::Regular { rank: _, suit } => suit.color(),
228 Self::Joker(color) => color,
229 }
230 }
231
232 /// Return `true` if this card is a Joker, otherwise return `false`
233 pub fn is_joker(self) -> bool {
234 match self {
235 Self::Regular { rank: _, suit: _ } => false,
236 Self::Joker(_) => true,
237 }
238 }
239}
240
241impl Rank {
242 /// Set of allowed chars which can be converted to [`Rank`] with
243 /// [`TryFrom<char>`]
244 pub const ALLOWED_CHARS: &'static str = "A23456789TJQK";
245}
246
247impl From<(Rank, Suit)> for Card {
248 fn from((rank, suit): (Rank, Suit)) -> Self {
249 Self::Regular{rank, suit}
250 }
251}
252
253/// Generate an (unshuffled) [shoe](https://en.wikipedia.org/wiki/Shoe_(cards))
254///
255/// Return an unshuffled shoe containing `n_decks` decks. If `with_jokers`
256/// `true`, Each deck has 54 cards, all the cards in a standard 52-card deck,
257/// and 2 Jokers, one Black and one Red. If `with_jokers` is `false`, each deck
258/// only contains the 52 cards in a standard deck.
259pub fn gen_shoe(n_decks: u8, with_jokers: bool) -> Vec<Card> {
260
261 let extra_cards: u8 = if with_jokers {2} else {0};
262 // total number of cards in the shoe
263 let total_cards = n_decks as usize * (CARDS_PER_DECK + extra_cards) as usize;
264
265 // preallocate memory for the shoe
266 let mut output = Vec::with_capacity(total_cards);
267 for _ in 0..n_decks {
268 let deck_iter = Rank::iter()
269 .cartesian_product(Suit::iter())
270 .map(Card::from);
271 output.extend(deck_iter);
272 if with_jokers {
273 output.extend([Card::Joker(Color::Red), Card::Joker(Color::Black)]);
274 }
275 }
276 output
277}
278
279#[cfg(test)]
280mod test {
281 use super::*;
282 use strum::IntoEnumIterator;
283 use itertools::Itertools;
284 use std::collections::HashMap;
285
286 pub const FULL_DECK_CARDS: [Card; 52] = [
287 Card::Regular{suit: Suit::Hearts, rank: Rank::Ace},
288 Card::Regular{suit: Suit::Hearts, rank: Rank::Two},
289 Card::Regular{suit: Suit::Hearts, rank: Rank::Three},
290 Card::Regular{suit: Suit::Hearts, rank: Rank::Four},
291 Card::Regular{suit: Suit::Hearts, rank: Rank::Five},
292 Card::Regular{suit: Suit::Hearts, rank: Rank::Six},
293 Card::Regular{suit: Suit::Hearts, rank: Rank::Seven},
294 Card::Regular{suit: Suit::Hearts, rank: Rank::Eight},
295 Card::Regular{suit: Suit::Hearts, rank: Rank::Nine},
296 Card::Regular{suit: Suit::Hearts, rank: Rank::Ten},
297 Card::Regular{suit: Suit::Hearts, rank: Rank::Jack},
298 Card::Regular{suit: Suit::Hearts, rank: Rank::Queen},
299 Card::Regular{suit: Suit::Hearts, rank: Rank::King},
300 Card::Regular{suit: Suit::Diamonds, rank: Rank::Ace},
301 Card::Regular{suit: Suit::Diamonds, rank: Rank::Two},
302 Card::Regular{suit: Suit::Diamonds, rank: Rank::Three},
303 Card::Regular{suit: Suit::Diamonds, rank: Rank::Four},
304 Card::Regular{suit: Suit::Diamonds, rank: Rank::Five},
305 Card::Regular{suit: Suit::Diamonds, rank: Rank::Six},
306 Card::Regular{suit: Suit::Diamonds, rank: Rank::Seven},
307 Card::Regular{suit: Suit::Diamonds, rank: Rank::Eight},
308 Card::Regular{suit: Suit::Diamonds, rank: Rank::Nine},
309 Card::Regular{suit: Suit::Diamonds, rank: Rank::Ten},
310 Card::Regular{suit: Suit::Diamonds, rank: Rank::Jack},
311 Card::Regular{suit: Suit::Diamonds, rank: Rank::Queen},
312 Card::Regular{suit: Suit::Diamonds, rank: Rank::King},
313 Card::Regular{suit: Suit::Spades, rank: Rank::Ace},
314 Card::Regular{suit: Suit::Spades, rank: Rank::Two},
315 Card::Regular{suit: Suit::Spades, rank: Rank::Three},
316 Card::Regular{suit: Suit::Spades, rank: Rank::Four},
317 Card::Regular{suit: Suit::Spades, rank: Rank::Five},
318 Card::Regular{suit: Suit::Spades, rank: Rank::Six},
319 Card::Regular{suit: Suit::Spades, rank: Rank::Seven},
320 Card::Regular{suit: Suit::Spades, rank: Rank::Eight},
321 Card::Regular{suit: Suit::Spades, rank: Rank::Nine},
322 Card::Regular{suit: Suit::Spades, rank: Rank::Ten},
323 Card::Regular{suit: Suit::Spades, rank: Rank::Jack},
324 Card::Regular{suit: Suit::Spades, rank: Rank::Queen},
325 Card::Regular{suit: Suit::Spades, rank: Rank::King},
326 Card::Regular{suit: Suit::Clubs, rank: Rank::Ace},
327 Card::Regular{suit: Suit::Clubs, rank: Rank::Two},
328 Card::Regular{suit: Suit::Clubs, rank: Rank::Three},
329 Card::Regular{suit: Suit::Clubs, rank: Rank::Four},
330 Card::Regular{suit: Suit::Clubs, rank: Rank::Five},
331 Card::Regular{suit: Suit::Clubs, rank: Rank::Six},
332 Card::Regular{suit: Suit::Clubs, rank: Rank::Seven},
333 Card::Regular{suit: Suit::Clubs, rank: Rank::Eight},
334 Card::Regular{suit: Suit::Clubs, rank: Rank::Nine},
335 Card::Regular{suit: Suit::Clubs, rank: Rank::Ten},
336 Card::Regular{suit: Suit::Clubs, rank: Rank::Jack},
337 Card::Regular{suit: Suit::Clubs, rank: Rank::Queen},
338 Card::Regular{suit: Suit::Clubs, rank: Rank::King},
339 ];
340
341 #[test]
342 /// Tests that all Ranks cast to their respective values of u8
343 fn rank_cast_u8() {
344 assert_eq!(Rank::Ace as u8, 1, "Ace as u8 was not 1");
345 assert_eq!(Rank::Two as u8, 2, "Two as u8 was not 2");
346 assert_eq!(Rank::Three as u8, 3, "Three as u8 was not 3");
347 assert_eq!(Rank::Four as u8, 4, "Four as u8 was not 4");
348 assert_eq!(Rank::Five as u8, 5, "Five as u8 was not 5");
349 assert_eq!(Rank::Six as u8, 6, "Six as u8 was not 6");
350 assert_eq!(Rank::Seven as u8, 7, "Seven as u8 was not 7");
351 assert_eq!(Rank::Eight as u8, 8, "Eight as u8 was not 8");
352 assert_eq!(Rank::Nine as u8, 9, "Nine as u8 was not 9");
353 assert_eq!(Rank::Ten as u8, 10, "Ten as u8 was not 10");
354 assert_eq!(Rank::Jack as u8, 11, "Jack as u8 was not 11");
355 assert_eq!(Rank::Queen as u8, 12, "Queen as u8 was not 12");
356 assert_eq!(Rank::King as u8, 13, "King as u8 was not 13");
357 }
358 #[test]
359 fn suit_colors() {
360 assert_eq!(Suit::Spades.color(), Color::Black);
361 assert_eq!(Suit::Hearts.color(), Color::Red);
362 assert_eq!(Suit::Clubs.color(), Color::Black);
363 assert_eq!(Suit::Diamonds.color(), Color::Red);
364 }
365 #[test]
366 fn card_colors() {
367 for card in FULL_DECK_CARDS[..26].iter() {
368 assert_eq!(card.color(), Color::Red, "{} incorrectly identified as Red", card);
369 }
370 for card in FULL_DECK_CARDS[26..].iter() {
371 assert_eq!(card.color(), Color::Black, "{} incorrectly identified as Black", card);
372 }
373 assert_eq!(Card::Joker(Color::Black).color(), Color::Black);
374 assert_eq!(Card::Joker(Color::Red).color(), Color::Red);
375 }
376 #[test]
377 fn is_joker() {
378 for card in FULL_DECK_CARDS.iter() {
379 assert!(!card.is_joker(), "{} incorrectly identified as joker", card)
380 }
381 assert!(Card::Joker(Color::Black).is_joker());
382 assert!(Card::Joker(Color::Red).is_joker());
383 }
384 fn count(shoe: &[Card]) -> HashMap<Card, u16> {
385 let mut map = HashMap::new();
386 for &card in shoe {
387 *map.entry(card).or_insert(0) += 1;
388 }
389 map
390 }
391 fn test_shoe(n: u8, w_jokers: bool) {
392 let shoe = gen_shoe(n, w_jokers);
393 let n_jokers = if w_jokers {2 * n} else {0} as usize;
394 assert_eq!(shoe.len(), CARDS_PER_DECK as usize * n as usize + n_jokers);
395 let map = count(&shoe);
396 for (rank, suit) in Rank::iter().cartesian_product(Suit::iter()) {
397 assert_eq!(map.get(&Card::Regular{rank, suit}), Some(&(n as u16)));
398 }
399 if w_jokers {
400 assert_eq!(map.get(&Card::Joker(Color::Black)), Some(&(n as u16)));
401 assert_eq!(map.get(&Card::Joker(Color::Red)), Some(&(n as u16)));
402 }
403 }
404 /// Assert that there is exactly 1 of each card in the results of `gen_shoe`.
405 #[test]
406 fn gen_shoe_no_jokers_single() {
407 test_shoe(1, false);
408 }
409 /// Assert that there is exactly N of each card in the results of `gen_shoe`.
410 #[test]
411 fn gen_shoe_no_jokers_multi() {
412 test_shoe(6, false);
413 }
414 #[test]
415 fn gen_shoe_w_jokers() {
416 test_shoe(1, true);
417 }
418 #[test]
419 fn gen_shoe_w_jokers_multi() {
420 test_shoe(4, true);
421 }
422}