Expand description
Cardpack is a library to represent various decks of playing cards. The library is designed to support the following features:
- Custom
RankandSuitPips. - Ability to sort a
DeckofCardsin various ways. - Localization of card names using fluent-templates.
§Overview
The structure of the library is the following:
Pile- A generic collection ofCardsthat implement theDeckedBasetrait
The library supports the following decks:
§French Deck
The French deck is the foundation Deck
of playing cards. It is made up of a collection of 54 Cards with 13 ranks in each of the four suits,
and two jokers. Most of the other decks are made up on the French BasicCards.
// ignored under cargo test --no-default-features (uses fluent_name* / FluentName at the bottom)
use cardpack::prelude::*;
let mut french_deck = Pile::<French>::deck();
// It's also possible to call the deck method directly on the specific generic implementing type:
let mut french_deck = French::deck();
assert_eq!(french_deck.len(), 54);
assert_eq!(
french_deck.to_string(),
"B🃟 L🃟 A♠ K♠ Q♠ J♠ T♠ 9♠ 8♠ 7♠ 6♠ 5♠ 4♠ 3♠ 2♠ A♥ K♥ Q♥ J♥ T♥ 9♥ 8♥ 7♥ 6♥ 5♥ 4♥ 3♥ 2♥ A♦ K♦ Q♦ J♦ T♦ 9♦ 8♦ 7♦ 6♦ 5♦ 4♦ 3♦ 2♦ A♣ K♣ Q♣ J♣ T♣ 9♣ 8♣ 7♣ 6♣ 5♣ 4♣ 3♣ 2♣"
);
assert!(french_deck.contains(&Card::<French>::new(FrenchBasicCard::ACE_SPADES)));
let shuffled = french_deck.shuffled();
// Use the `french_cards!` macro to parse the shuffled deck as a string:
let parsed = french_cards!(shuffled.to_string().as_str());
// Verify that the cards, in any order, are the same:
assert!(french_deck.same(&parsed));
// When sorted, they should be exactly the same:
assert_eq!(parsed.sorted(), french_deck);
// For a joker card's index string, `B` stands for the Big or Full-Color Joker and `L` for the
// Little or One-Color Joker, with `🃟` being the symbol character for the joker suit.
let jokers = french_deck.draw(2).unwrap();
assert_eq!(jokers.to_string(), "B🃟 L🃟");
let royal_flush = french_deck.draw(5).unwrap();
assert_eq!(royal_flush.to_string(), "A♠ K♠ Q♠ J♠ T♠");
assert_eq!(royal_flush.index(), "AS KS QS JS TS");
// The original deck should now have five cards less:
assert_eq!(french_deck.len(), 47);
// Cards can provide a longer description in English and German:
assert_eq!(Card::<French>::new(FrenchBasicCard::ACE_SPADES).fluent_name_default(), "Ace of Spades");
assert_eq!(Card::<French>::new(FrenchBasicCard::QUEEN_HEARTS).fluent_name(&FluentName::DEUTSCH), "Dame Herzen");At some point I would love to add support for more languages.
§Standard 52 Card Deck
A Standard52 deck is a
French deck without the two jokers.
use cardpack::prelude::*;
let mut standard52_deck = Pile::<Standard52>::deck();
assert_eq!(standard52_deck.len(), 52);
assert_eq!(
standard52_deck.to_string(),
"A♠ K♠ Q♠ J♠ T♠ 9♠ 8♠ 7♠ 6♠ 5♠ 4♠ 3♠ 2♠ A♥ K♥ Q♥ J♥ T♥ 9♥ 8♥ 7♥ 6♥ 5♥ 4♥ 3♥ 2♥ A♦ K♦ Q♦ J♦ T♦ 9♦ 8♦ 7♦ 6♦ 5♦ 4♦ 3♦ 2♦ A♣ K♣ Q♣ J♣ T♣ 9♣ 8♣ 7♣ 6♣ 5♣ 4♣ 3♣ 2♣"
);
// It includes the card! and cards! macros for easy Standard52 card creation:
assert_eq!(card!(AS), Card::<Standard52>::new(FrenchBasicCard::ACE_SPADES));
assert_eq!(cards!("AS KS QS JS TS"), standard52_deck.draw(5).unwrap());By default, a Deck displays the suit symbols when you display the
values. It also has the ability to return the letter values, or what are called “index strings”.
use cardpack::prelude::*;
assert_eq!(
Pile::<Standard52>::deck().index(),
"AS KS QS JS TS 9S 8S 7S 6S 5S 4S 3S 2S AH KH QH JH TH 9H 8H 7H 6H 5H 4H 3H 2H AD KD QD JD TD 9D 8D 7D 6D 5D 4D 3D 2D AC KC QC JC TC 9C 8C 7C 6C 5C 4C 3C 2C"
);An important thing to remember about the decks is that the cards have their weight inside them
to facilitate sorting. If you wanted a deck for a game of poker where the lowest hand wins, you
would need to create a separate deck file with the card’s Rank weights inverted. The
Razz Deck is an example of this. It is also an example of
how you can create a Deck where the
BasicCard for the deck are generated programmatically
in YAML instead using the power of Serde
// ignored under cargo test --no-default-features (Razz needs the `yaml` feature)
use cardpack::prelude::*;
assert_eq!(Pile::<Razz>::deck().draw(5).unwrap().to_string(), "A♠ 2♠ 3♠ 4♠ 5♠");
assert_eq!(Pile::<Standard52>::deck().draw(5).unwrap().to_string(), "A♠ K♠ Q♠ J♠ T♠");The raw YAML that was used to create the Razz Deck is available
in the source code.
Other decks include:
Canasta- 2 Modern decks with the red 3s made jokers.Euchre24- A 24 card version of a Euchre deck.Euchre32- A 32 card version of a Euchre deck.ShortDeck- A 36 card deck with ranks 6 through Ace.Pinochle- A 48 card deck with two copies of the 9 through Ace ranks.Skat- A 32 card German card game with different suits and ranks.Spades- A Modern deck with the 2 of Clubs and 2 of Diamonds removed.Tarot- A 78 card deck with 22 Major Arcana and 56 Minor Arcana cards.
In past versions of the library there was a Hand and Foot
deck. This has been removed because it can simply be created using a
French and what functionality is available in the Decked trait:
use cardpack::prelude::*;
let hand_and_foot_4players = French::decks(4);
assert_eq!(hand_and_foot_4players.len(), 216);
let hand_and_foot_5players = French::decks(5);
assert_eq!(hand_and_foot_5players.len(), 270);§Custom Deck example:
Here’s a very simple example where we create a tiny deck with only the ace and kink ranks,
and only the spades and hearts suits. Just for fun, we’ll include a tiny! macro for one Tiny card.
// ignored under cargo test --no-default-features (uses colored::Color directly)
use std::collections::HashMap;
use colored::Color;
use cardpack::prelude::*;
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Tiny {}
impl Tiny {
pub const DECK_SIZE: usize = 4;
pub const DECK: [BasicCard; Tiny::DECK_SIZE] = [
FrenchBasicCard::ACE_SPADES,
FrenchBasicCard::KING_SPADES,
FrenchBasicCard::ACE_HEARTS,
FrenchBasicCard::KING_HEARTS,
];
}
impl DeckedBase for Tiny {
fn base_vec() -> Vec<BasicCard> {
Tiny::DECK.to_vec()
}
fn colors() -> HashMap<Pip, Color> {
Standard52::colors()
}
fn deck_name() -> String {
"Tiny".to_string()
}
fn fluent_deck_key() -> String {
FLUENT_KEY_BASE_NAME_FRENCH.to_string()
}
}
// Let's you call Decked methods directly on the Tiny type:
impl Decked<Tiny> for Tiny {}
macro_rules! tiny {
(AS) => {
Card::<Tiny>::new(FrenchBasicCard::ACE_SPADES)
};
(KS) => {
Card::<Tiny>::new(FrenchBasicCard::KING_SPADES)
};
(AH) => {
Card::<Tiny>::new(FrenchBasicCard::ACE_HEARTS)
};
(KH) => {
Card::<Tiny>::new(FrenchBasicCard::KING_HEARTS)
};
(__) => {
Card::<Tiny>::default()
};
}
let mut deck = Tiny::deck();
assert_eq!(deck.to_string(), "A♠ K♠ A♥ K♥");
// Every deck comes with the Ranged trait automatically:
assert_eq!(deck.combos(2).to_string(), "A♠ K♠, A♠ A♥, A♠ K♥, K♠ K♥, A♥ K♠, A♥ K♥");
// Deal from the top of the deck:
assert_eq!(deck.draw_first().unwrap().to_string(), "A♠");
// Deal from the bottom of the deck:
assert_eq!(deck.draw_last().unwrap().to_string(), "K♥");
// Should be two cards remaining:
assert_eq!(deck.len(), 2);
assert_eq!(deck.index(), "KS AH");
// Draw a remaining card:
assert_eq!(deck.draw_first().unwrap(), tiny!(KS));
// Draw the last card:
assert_eq!(deck.draw_last().unwrap(), tiny!(AH));
// And now the deck is empty:
assert!(deck.draw_first().is_none());
assert!(deck.draw_last().is_none());
// Of all the tests you could use to make sure that your deck is setup correctly, the most
// fundamental is the validate method.
assert!(Tiny::validate());§Decks as YAML
With the yaml feature every deck round-trips through YAML —
deck → YAML → deck — using a self-describing envelope that carries the
deck’s identity alongside its cards, rather than a bare card list:
version: 1
name: Tiny
fluent_deck_key: french
count: 4
cards:
- suit: { weight: 3, pip_type: Suit, index: 'S', symbol: '♠', value: 4 }
rank: { weight: 12, pip_type: Rank, index: 'A', symbol: 'A', value: 11 }
# ...There are three entry points, one per layer:
YamlDecked— blanket-implemented for everyDeckedBasetype, so theTinydeck defined above getsto_yaml,deck_from_yaml, andvalidate_yamlfor free, exactly like the decks shipped in this crate.DeckKind— the non-generic path: serialize a deck you only know at runtime, and parse a document back into the registry variant that produced it.Pile<T>— the instance path. Order is preserved, so a shuffled deck or a dealt hand round-trips as-is. Unlike the deck-level paths, aPilemay legitimately be empty (fully drawn) and need not hold every card in the deck; membership, not cardinality, is the rule.
// ignored under cargo test --no-default-features (needs the `yaml` feature)
use cardpack::prelude::*;
// Type level: any DeckedBase implementor, including your own.
let yaml = French::to_yaml().unwrap();
assert_eq!(French::deck_from_yaml(&yaml).unwrap(), French::base_vec());
// A well-formed document describing the *wrong* deck is still an error:
assert!(Tarot::validate_yaml(&yaml).is_err());
// Registry level: round-trips through the deck's identity.
for kind in DeckKind::all() {
assert_eq!(DeckKind::from_yaml(&kind.to_yaml().unwrap()).unwrap(), *kind);
}
// Instance level: order survives, and so does a partial hand.
let shuffled = Pile::<Standard52>::deck().shuffled_with_seed(42);
let restored = Pile::<Standard52>::from_yaml(&shuffled.to_yaml().unwrap()).unwrap();
assert_eq!(restored, shuffled);The reader accepts the legacy bare-sequence form as well — the format
BasicCard::cards_from_yaml_str has always taken, and the one
src/basic/decks/yaml/razz.yaml is written in — so envelope support is a
strict superset of what came before.
§cardpack.rs
Generic pack of cards library written in Rust. The goals of the library include:
- Various types of decks of cards.
- Internationalization support.
- Ability to create custom sorts for a specific pack of cards.
UPDATE: This is a complete rewrite of the library taking advantage of generics in order to make the code cleaner, and easier to extend.
§Setup
Build and run common tasks with GNU make:
makeRun make help to see all available targets.
§Usage
use cardpack::prelude::*;
fn main() {
let mut pack = Standard52::deck();
// Deterministic shuffle — works in the pure, `no_std` default build.
// With the `std` feature you can call `pack.shuffle()` for a thread-RNG shuffle.
pack.shuffle_with_seed(42);
// Deal no-limit hold'em hands for two players:
let small_blind = pack.draw(2).unwrap().sorted_by_rank();
let big_blind = pack.draw(2).unwrap().sorted_by_rank();
println!("small blind: {}", small_blind.to_string());
println!("big blind: {}", big_blind.to_string());
let flop = pack.draw(3).unwrap();
let turn = pack.draw(1).unwrap();
let river = pack.draw(1).unwrap();
println!();
println!("flop : {}", flop.to_string());
println!("turn : {}", turn.to_string());
println!("river: {}", river.to_string());
// Now, let's validate that the cards when collected back together are a valid Standard52
// deck of cards.
let reconstituted_pile =
Pile::<Standard52>::pile_on(&*vec![pack, small_blind, big_blind, flop, turn, river]);
assert!(Standard52::deck().same(&reconstituted_pile));
}§Details
The goal of this library is to be able to support the creation of card decks of various sizes and suits. Out of the box, the library supports:
- French Deck
- Ganjifa with per-suit inverted pip ranking
- Mughal (8 suits × 12 = 96 cards)
- Dashavatara (10 suits × 12 = 120 cards)
- Short Deck
- Skat
- Tarot with Major and Minor Arcana
The project takes advantage of Project Fluent’s Rust support to offer internationalization. Current languages supported are English, German, French, Latin, and Klingon.
§Cargo features
cardpack is pure by default: a bare dependency is an alloc-only,
no_std, no-I/O domain kernel. Every dependency-bearing or I/O-bearing
capability is gated behind a Cargo feature, so consumers opt in to exactly
what they need:
| Feature | Default | Pulls in | What it turns on |
|---|---|---|---|
full | no | everything below | Umbrella turning on std + i18n + colored-display + yaml + serde |
std | no | libstd | std-only APIs (thread-RNG shuffle, draw_random, etc.) |
i18n | no | fluent-templates | FluentName, Named, Card::fluent_name*, localization |
colored-display | no | colored | Color, Colorize, Card::color*, Pile::to_color_* |
yaml | no | serde_norway | Full deck ↔ YAML round-tripping (pure, in-memory) — see Decks as YAML; plus the Razz deck |
serde | no | serde | Serialize/Deserialize derives on Pip/Card/Pile etc. |
std-io | no | — | BasicCard::cards_from_yaml_file — reads decks from YAML files (std::fs). The crate’s one filesystem seam; not in full |
funky | no | std | The Balatro-style engine — see Funky below |
To get the previous “batteries-included” behavior, opt into full:
# Full convenience stack (i18n, colored display, YAML, serde):
cardpack = { version = "0.8", features = ["full"] }
# Or trim to just what you need — e.g. the pure kernel plus serde:
cardpack = { version = "0.8", features = ["serde"] }
# Or the pure, no_std, alloc-only kernel with no extra deps at all:
cardpack = "0.8"yaml implies serde (it deserializes into the serde-derived structs).
std-io implies yaml and adds the filesystem reader on top of it; it is the
only feature that lets the crate touch std::fs, and it is intentionally left
out of full so the pure kernel and the convenience stack both stay I/O-free.
§Decks as YAML
With yaml, every deck round-trips deck → YAML → deck. Documents use a
self-describing envelope that carries the deck’s identity — version,
name, fluent_deck_key, count, cards — rather than a bare card list, so
a document can be checked against the deck it claims to be. The reader still
accepts the legacy bare sequence, so the new format is a strict superset of
what BasicCard::cards_from_yaml_str always took.
// This README is included in the crate docs, so its code blocks are compiled
// as doctests. Ignored because it needs the `yaml` feature, which is off by
// default; the executable versions live on the `YamlDecked` methods.
use cardpack::prelude::*;
// Any DeckedBase implementor — including a deck you wrote — gets this free
// via the blanket `YamlDecked` trait:
let yaml = French::to_yaml().unwrap();
assert_eq!(French::deck_from_yaml(&yaml).unwrap(), French::base_vec());
// A well-formed document describing the wrong deck is still rejected:
assert!(Tarot::validate_yaml(&yaml).is_err());
// `Pile` serialization preserves order, so hands and shuffles survive intact:
let shuffled = Pile::<Standard52>::deck().shuffled_with_seed(42);
let restored = Pile::<Standard52>::from_yaml(&shuffled.to_yaml().unwrap()).unwrap();
assert_eq!(restored, shuffled);DeckKind::to_yaml / DeckKind::from_yaml cover the non-generic path, for
decks known only at runtime. Golden fixtures for all shipped decks live in
tests/fixtures/yaml/ and are regenerated with make yaml-fixtures.
§Funky — Balatro-style cards
The funky feature is a result of having my mind blown🤯 playing the
amazing solitare game Balatro. It honestly
changed the way I look at playing cards. Suddenly, suits and ranks are
just two of an infinite possible number of pips that can be attached to a
“playing card”. I started realizing that there is little difference between
a French Deck of cards and creating heros in the
Evercraft Kata.
The goal of the feature is to see how hard I need to push the architecture of
this library to support decks such as those in Balatro. I guess the big idea
was the MPip, a sort of functional version of a pip on a card.
TBH, this experiment demonstrates the rational behind designing games in flexible languages such as Lua, over tyrannical ones such as my beloved Rust.
There are a couple of use cases that are in the back of my mind for something like this. One is a Balatro score solver, as a way to teach the math mechanics behind the game. The other is a library that would be able to create modded Balatro decks from simple yaml configuration files, similar to what the library already supports in simpler decks.
It is still very much a work in progress, which is documented here:
docs/EPIC-01_Funky.md.
There are two examples to see it in action:
# The four-phase scoring pipeline, phase by phase:
cargo ex buffoon
# A seeded four-act tour — round loop, editions, shop & vouchers, spectrals:
cargo ex funky_tour§WebAssembly
cardpack compiles cleanly to wasm32-unknown-unknown (browser WASM)
with every feature combination. See docs/wasm.md for
the consumer-side getrandom backend setup, recommended feature
combos, and runtime gotchas. A working example lives at
examples/wasm.rs.
§Responsibilities
- Represent a specific type of card deck.
- Validate that a collection of cards is valid for that type of deck.
- Create a textual representation of a deck that can be serialized and deserialized.
- Shuffle a deck
§Examples
The library has several examples programs, including demo which shows you the different decks
available.
Run them with cargo ex <name>. Because cardpack is pure by default
(default = [], see Cargo features), most examples need
--features to compile; cargo ex is an alias in
.cargo/config.toml that supplies them for you, so
cargo ex demo beats cargo run --features full,funky --example demo.
For the traditional 54 card French Deck with Jokers:
❯ cargo ex demo -- --french -v
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.06s
Running `target/debug/examples/demo --french -v`
French Deck: B🃟 L🃟 A♠ K♠ Q♠ J♠ T♠ 9♠ 8♠ 7♠ 6♠ 5♠ 4♠ 3♠ 2♠ A♥ K♥ Q♥ J♥ T♥ 9♥ 8♥ 7♥ 6♥ 5♥ 4♥ 3♥ 2♥ A♦ K♦ Q♦ J♦ T♦ 9♦ 8♦ 7♦ 6♦ 5♦ 4♦ 3♦ 2♦ A♣ K♣ Q♣ J♣ T♣ 9♣ 8♣ 7♣ 6♣ 5♣ 4♣ 3♣ 2♣
French Deck Index: BJ LJ AS KS QS JS TS 9S 8S 7S 6S 5S 4S 3S 2S AH KH QH JH TH 9H 8H 7H 6H 5H 4H 3H 2H AD KD QD JD TD 9D 8D 7D 6D 5D 4D 3D 2D AC KC QC JC TC 9C 8C 7C 6C 5C 4C 3C 2C
French Deck Shuffled: K♣ 7♦ 8♣ Q♥ 6♠ J♦ 4♦ J♥ K♠ 9♥ 6♥ T♥ 2♦ 3♦ 3♣ J♣ 3♥ Q♣ 5♥ Q♦ 3♠ T♣ 7♥ 4♥ K♦ 5♦ 2♠ 6♦ T♠ 8♥ T♦ 7♠ 8♠ 2♣ Q♠ 7♣ A♣ 5♠ A♥ 9♣ 2♥ 9♦ 9♠ 4♠ K♥ 8♦ 5♣ A♦ L🃟 B🃟 A♠ 6♣ 4♣ J♠
English | German | French | Latin | Klingon
------------------------ | ------------------------ | ------------------------ | ------------------------ | ------------------------
Joker Full-Color | Joker Großer | Joker Grand | Joker Magnus | Joker qoH'a'
Joker One-Color | Joker Kleiner | Joker Petit | Joker Parvus | Joker qoHHom
Ace of Spades | Ass Spaten | As de Pique | As Spathae | wa'DIch yan
King of Spades | König Spaten | Roi de Pique | Rex Spathae | ta' yan
Queen of Spades | Dame Spaten | Dame de Pique | Regina Spathae | ta'be' yan
Jack of Spades | Bube Spaten | Valet de Pique | Famulus Spathae | toy'wI' yan
Ten of Spades | Zhen Spaten | Dix de Pique | Decem Spathae | wa'maH yan
...Display a hand of Bridge:
❯ cargo ex bridge
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.33s
Running `target/debug/examples/bridge`
First, let's deal out a random bridge hand.
Here it is in Portable Bridge Notation:
W:KJT.JT63.K8.QJT9 A75.KQ9874.65.AK Q6432.5.AJ74.853 98.A2.QT932.7642
How does it look as a traditional compass?
NORTH
♠ A 7 5
♥ K Q 9 8 7 4
♦ 6 5
♣ A K
WEST EAST
♠ K J T ♠ Q 6 4 3 2
♥ J T 6 3 ♥ 5
♦ K 8 ♦ A J 7 4
♣ Q J T 9 ♣ 8 5 3
SOUTH
♠ 9 8
♥ A 2
♦ Q T 9 3 2
♣ 7 6 4 2
Now, let's take a PBN Deal String and convert it into a bridge hand.
Here's the original' Portable Bridge Notation:
S:Q42.Q52.AQT943.Q 97.AT93.652.T743 AJT85.J76.KJ.A65 K63.K84.87.KJ982
As a bridge compass:
NORTH
♠ A J T 8 5
♥ J 7 6
♦ K J
♣ A 6 5
WEST EAST
♠ 9 7 ♠ K 6 3
♥ A T 9 3 ♥ K 8 4
♦ 6 5 2 ♦ 8 7
♣ T 7 4 3 ♣ K J 9 8 2
SOUTH
♠ Q 4 2
♥ Q 5 2
♦ A Q T 9 4 3
♣ Q
Other decks in the demo program are canasta, euchre, short, pinochle, skat, spades,
standard, tarot, mughal, and dashavatara.
Other examples are:
cargo ex handandfoot- Shows how to support more than one decks like in the game Hand and Foot.cargo ex poker- A random heads up no-limit Poker deal.cargo ex poker_eval- Scores a Texas Hold’em board via theckc-rsevaluator, picking each player’s best 5-card hand from their 7.cargo ex range- Prints a 13×13 starting-hand range chart.cargo ex buffoon- The Balatro four-phase scoring pipeline, phase by phase (see Funky).cargo ex funky_tour- A seeded tour of the funky engine: round loop, editions, shop & vouchers, spectral cards.cargo build --target wasm32-unknown-unknown --example wasm- Minimal browser-WASM build showing wasm-friendly API patterns (seeded shuffle, no filesystem). Seedocs/wasm.md.
§References
§Other Deck of Cards Libraries
- ascclemens/cards
- locka99/deckofcards-rs
- vsupalov/cards-rs
- droundy/bridge-cards
- Tarot Libraries
§Dependencies
§Dev Dependencies
- term-table
- rstest - Fixture-based test framework for Rust
§TODO
Modules§
Macros§
- basic
- basic_
cell - bcard
- bcards
- card
- This macro is to allow for quick and easy generation of individual cards from the most common
Standard52deck. - cards
- A macro to create a Pile of Standard52 cards from a string.
- french_
cards - A macro to create a Pile of
French Deckcards from a string.