1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![doc = include_str!("../README.md")]
3#![doc(html_logo_url = "https://codeberg.org/pezcore/playin-cards/raw/branch/main/icon.svg")]
4#[allow(unused_imports)]
16use std::fmt::{Display, UpperHex};
17
18use itertools::Itertools;
19use strum::EnumIter;
20use strum::IntoEnumIterator;
21
22pub const CARDS_PER_DECK: u8 = 52;
24
25#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
67pub enum Card {
68 Regular { rank: Rank, suit: Suit },
70 Joker(Color),
72}
73
74#[repr(u8)]
148#[derive(EnumIter, Debug, Ord, PartialOrd, PartialEq, Clone, Copy, Eq, Hash)]
149pub enum Rank {
150 Two = 2,
151 Three,
152 Four,
153 Five,
154 Six,
155 Seven,
156 Eight,
157 Nine,
158 Ten,
159 Jack,
160 Queen,
161 King,
162 Ace,
163}
164
165#[derive(EnumIter, Debug, Clone, Copy, PartialEq, Eq, Hash)]
196pub enum Suit {
197 Hearts,
198 Diamonds,
199 Spades,
200 Clubs,
201}
202
203#[derive(Debug, PartialEq, Clone, Copy, Eq, Hash)]
205pub enum Color {
206 Red,
207 Black,
208}
209
210mod format;
211mod parse;
212
213pub use parse::ParseError;
214
215impl Suit {
216 pub const ALLOWED_CHARS: &'static str = "SsCcHhDd♠♣♥♦";
219
220 pub fn color(self) -> Color {
222 match self {
223 Suit::Hearts | Suit::Diamonds => Color::Red,
224 Suit::Spades | Suit::Clubs => Color::Black,
225 }
226 }
227}
228
229impl Card {
230 pub fn color(self) -> Color {
232 match self {
233 Self::Regular { rank: _, suit } => suit.color(),
234 Self::Joker(color) => color,
235 }
236 }
237
238 pub fn is_joker(self) -> bool {
240 match self {
241 Self::Regular { rank: _, suit: _ } => false,
242 Self::Joker(_) => true,
243 }
244 }
245}
246
247impl Rank {
248 pub const ALLOWED_CHARS: &'static str = "A23456789TJQK";
251
252 pub fn is_adjacent(self, other: Self) -> bool {
255 matches!((self as u8).abs_diff(other as u8), 1 | 12)
256 }
257}
258
259impl From<(Rank, Suit)> for Card {
260 fn from((rank, suit): (Rank, Suit)) -> Self {
261 Self::Regular { rank, suit }
262 }
263}
264
265pub fn gen_shoe(n_decks: u8, with_jokers: bool) -> Vec<Card> {
272 let extra_cards: u8 = if with_jokers { 2 } else { 0 };
273 let total_cards = n_decks as usize * (CARDS_PER_DECK + extra_cards) as usize;
275
276 let mut output = Vec::with_capacity(total_cards);
278 for _ in 0..n_decks {
279 let deck_iter = Rank::iter().cartesian_product(Suit::iter()).map(Card::from);
280 output.extend(deck_iter);
281 if with_jokers {
282 output.extend([Card::Joker(Color::Red), Card::Joker(Color::Black)]);
283 }
284 }
285 output
286}
287
288#[cfg(test)]
289mod test {
290 use super::*;
291 use itertools::Itertools;
292 use std::collections::HashMap;
293 use strum::IntoEnumIterator;
294
295 #[rustfmt::skip]
296 pub const FULL_DECK_CARDS: [Card; 52] = [
297 Card::Regular{ suit: Suit::Hearts, rank: Rank::Ace },
298 Card::Regular{ suit: Suit::Hearts, rank: Rank::Two },
299 Card::Regular{ suit: Suit::Hearts, rank: Rank::Three },
300 Card::Regular{ suit: Suit::Hearts, rank: Rank::Four },
301 Card::Regular{ suit: Suit::Hearts, rank: Rank::Five },
302 Card::Regular{ suit: Suit::Hearts, rank: Rank::Six },
303 Card::Regular{ suit: Suit::Hearts, rank: Rank::Seven },
304 Card::Regular{ suit: Suit::Hearts, rank: Rank::Eight },
305 Card::Regular{ suit: Suit::Hearts, rank: Rank::Nine },
306 Card::Regular{ suit: Suit::Hearts, rank: Rank::Ten },
307 Card::Regular{ suit: Suit::Hearts, rank: Rank::Jack },
308 Card::Regular{ suit: Suit::Hearts, rank: Rank::Queen },
309 Card::Regular{ suit: Suit::Hearts, rank: Rank::King },
310 Card::Regular{ suit: Suit::Diamonds, rank: Rank::Ace },
311 Card::Regular{ suit: Suit::Diamonds, rank: Rank::Two },
312 Card::Regular{ suit: Suit::Diamonds, rank: Rank::Three },
313 Card::Regular{ suit: Suit::Diamonds, rank: Rank::Four },
314 Card::Regular{ suit: Suit::Diamonds, rank: Rank::Five },
315 Card::Regular{ suit: Suit::Diamonds, rank: Rank::Six },
316 Card::Regular{ suit: Suit::Diamonds, rank: Rank::Seven },
317 Card::Regular{ suit: Suit::Diamonds, rank: Rank::Eight },
318 Card::Regular{ suit: Suit::Diamonds, rank: Rank::Nine },
319 Card::Regular{ suit: Suit::Diamonds, rank: Rank::Ten },
320 Card::Regular{ suit: Suit::Diamonds, rank: Rank::Jack },
321 Card::Regular{ suit: Suit::Diamonds, rank: Rank::Queen },
322 Card::Regular{ suit: Suit::Diamonds, rank: Rank::King },
323 Card::Regular{ suit: Suit::Spades, rank: Rank::Ace },
324 Card::Regular{ suit: Suit::Spades, rank: Rank::Two },
325 Card::Regular{ suit: Suit::Spades, rank: Rank::Three },
326 Card::Regular{ suit: Suit::Spades, rank: Rank::Four },
327 Card::Regular{ suit: Suit::Spades, rank: Rank::Five },
328 Card::Regular{ suit: Suit::Spades, rank: Rank::Six },
329 Card::Regular{ suit: Suit::Spades, rank: Rank::Seven },
330 Card::Regular{ suit: Suit::Spades, rank: Rank::Eight },
331 Card::Regular{ suit: Suit::Spades, rank: Rank::Nine },
332 Card::Regular{ suit: Suit::Spades, rank: Rank::Ten },
333 Card::Regular{ suit: Suit::Spades, rank: Rank::Jack },
334 Card::Regular{ suit: Suit::Spades, rank: Rank::Queen },
335 Card::Regular{ suit: Suit::Spades, rank: Rank::King },
336 Card::Regular{ suit: Suit::Clubs, rank: Rank::Ace },
337 Card::Regular{ suit: Suit::Clubs, rank: Rank::Two },
338 Card::Regular{ suit: Suit::Clubs, rank: Rank::Three },
339 Card::Regular{ suit: Suit::Clubs, rank: Rank::Four },
340 Card::Regular{ suit: Suit::Clubs, rank: Rank::Five },
341 Card::Regular{ suit: Suit::Clubs, rank: Rank::Six },
342 Card::Regular{ suit: Suit::Clubs, rank: Rank::Seven },
343 Card::Regular{ suit: Suit::Clubs, rank: Rank::Eight },
344 Card::Regular{ suit: Suit::Clubs, rank: Rank::Nine },
345 Card::Regular{ suit: Suit::Clubs, rank: Rank::Ten },
346 Card::Regular{ suit: Suit::Clubs, rank: Rank::Jack },
347 Card::Regular{ suit: Suit::Clubs, rank: Rank::Queen },
348 Card::Regular{ suit: Suit::Clubs, rank: Rank::King },
349 ];
350
351 #[test]
352 fn rank_cast_u8() {
354 assert_eq!(Rank::Ace as u8, 14, "Ace as u8 was not 14");
355 assert_eq!(Rank::Two as u8, 2, "Two as u8 was not 2");
356 assert_eq!(Rank::Three as u8, 3, "Three as u8 was not 3");
357 assert_eq!(Rank::Four as u8, 4, "Four as u8 was not 4");
358 assert_eq!(Rank::Five as u8, 5, "Five as u8 was not 5");
359 assert_eq!(Rank::Six as u8, 6, "Six as u8 was not 6");
360 assert_eq!(Rank::Seven as u8, 7, "Seven as u8 was not 7");
361 assert_eq!(Rank::Eight as u8, 8, "Eight as u8 was not 8");
362 assert_eq!(Rank::Nine as u8, 9, "Nine as u8 was not 9");
363 assert_eq!(Rank::Ten as u8, 10, "Ten as u8 was not 10");
364 assert_eq!(Rank::Jack as u8, 11, "Jack as u8 was not 11");
365 assert_eq!(Rank::Queen as u8, 12, "Queen as u8 was not 12");
366 assert_eq!(Rank::King as u8, 13, "King as u8 was not 13");
367 }
368 #[test]
369 fn suit_colors() {
370 assert_eq!(Suit::Spades.color(), Color::Black);
371 assert_eq!(Suit::Hearts.color(), Color::Red);
372 assert_eq!(Suit::Clubs.color(), Color::Black);
373 assert_eq!(Suit::Diamonds.color(), Color::Red);
374 }
375 #[test]
376 fn card_colors() {
377 for card in FULL_DECK_CARDS[..26].iter() {
378 assert_eq!(
379 card.color(),
380 Color::Red,
381 "{} incorrectly identified as Red",
382 card
383 );
384 }
385 for card in FULL_DECK_CARDS[26..].iter() {
386 assert_eq!(
387 card.color(),
388 Color::Black,
389 "{} incorrectly identified as Black",
390 card
391 );
392 }
393 assert_eq!(Card::Joker(Color::Black).color(), Color::Black);
394 assert_eq!(Card::Joker(Color::Red).color(), Color::Red);
395 }
396 #[test]
397 fn is_joker() {
398 for card in FULL_DECK_CARDS.iter() {
399 assert!(!card.is_joker(), "{} incorrectly identified as joker", card)
400 }
401 assert!(Card::Joker(Color::Black).is_joker());
402 assert!(Card::Joker(Color::Red).is_joker());
403 }
404 fn count(shoe: &[Card]) -> HashMap<Card, u16> {
405 let mut map = HashMap::new();
406 for &card in shoe {
407 *map.entry(card).or_insert(0) += 1;
408 }
409 map
410 }
411 fn test_shoe(n: u8, w_jokers: bool) {
412 let shoe = gen_shoe(n, w_jokers);
413 let n_jokers = if w_jokers { 2 * n } else { 0 } as usize;
414 assert_eq!(shoe.len(), CARDS_PER_DECK as usize * n as usize + n_jokers);
415 let map = count(&shoe);
416 for (rank, suit) in Rank::iter().cartesian_product(Suit::iter()) {
417 assert_eq!(map.get(&Card::Regular { rank, suit }), Some(&(n as u16)));
418 }
419 if w_jokers {
420 assert_eq!(map.get(&Card::Joker(Color::Black)), Some(&(n as u16)));
421 assert_eq!(map.get(&Card::Joker(Color::Red)), Some(&(n as u16)));
422 }
423 }
424 #[test]
426 fn gen_shoe_no_jokers_single() {
427 test_shoe(1, false);
428 }
429 #[test]
431 fn gen_shoe_no_jokers_multi() {
432 test_shoe(6, false);
433 }
434 #[test]
435 fn gen_shoe_w_jokers() {
436 test_shoe(1, true);
437 }
438 #[test]
439 fn gen_shoe_w_jokers_multi() {
440 test_shoe(4, true);
441 }
442
443 #[rustfmt::skip]
444 #[test]
445 fn is_adjacent() {
446 for (r1, r2) in Rank::iter().cartesian_product(Rank::iter()) {
447 match (r1, r2) {
448 (Rank::Ace, Rank::Two) | (Rank::Two, Rank::Ace) => assert!(r1.is_adjacent(r2)),
449 (Rank::Two, Rank::Three) | (Rank::Three, Rank::Two) => assert!(r1.is_adjacent(r2)),
450 (Rank::Three, Rank::Four) | (Rank::Four, Rank::Three) => assert!(r1.is_adjacent(r2)),
451 (Rank::Four, Rank::Five) | (Rank::Five, Rank::Four) => assert!(r1.is_adjacent(r2)),
452 (Rank::Five, Rank::Six) | (Rank::Six, Rank::Five) => assert!(r1.is_adjacent(r2)),
453 (Rank::Six, Rank::Seven) | (Rank::Seven, Rank::Six) => assert!(r1.is_adjacent(r2)),
454 (Rank::Seven, Rank::Eight) | (Rank::Eight, Rank::Seven) => assert!(r1.is_adjacent(r2)),
455 (Rank::Eight, Rank::Nine) | (Rank::Nine, Rank::Eight) => assert!(r1.is_adjacent(r2)),
456 (Rank::Nine, Rank::Ten) | (Rank::Ten, Rank::Nine) => assert!(r1.is_adjacent(r2)),
457 (Rank::Ten, Rank::Jack) | (Rank::Jack, Rank::Ten) => assert!(r1.is_adjacent(r2)),
458 (Rank::Jack, Rank::Queen) | (Rank::Queen, Rank::Jack) => assert!(r1.is_adjacent(r2)),
459 (Rank::Queen, Rank::King) | (Rank::King, Rank::Queen) => assert!(r1.is_adjacent(r2)),
460 (Rank::King, Rank::Ace) | (Rank::Ace, Rank::King) => assert!(r1.is_adjacent(r2)),
461 _ => assert!(!r1.is_adjacent(r2)),
462 }
463 }
464 }
465}