cardpack.rs Knowledge Bundle
Bundle OKF 0.1 · 6 conceitos · ImperialBower/cardpack.rs
Open source Repository Open in the app JSON README (API)
About
# cardpack.rs Knowledge Bundle
* [Getting started](getting-started.md) - how this bundle is organized and where to start reading.
# Sections
* [Architecture](architecture/) - crate design: the card model, feature flags, kernel purity, funky engine, localization.
* [Decks](decks/) - the 14 shipped deck kinds and how to author custom decks.
* [Workflows](workflows/) - build/test/quality gates and WebAssembly support.
* [Decisions](decisions/) - load-bearing decisions that are easy to accidentally undo.
* [References](references/) - map of the in-repo documentation this bundle distills, plus a pointer concept for each mirrored document.
Details
- Kind
- OKF bundles
- Topic
- Maps, weather & travel
- Publisher
- imperialbower
- Origin
- okf_github
- Category
- dados
- Version
- 0.1
- Stars
- 24
- Forks
- 6
- Open pull requests
- 1
- Last push
- 2026-09-05T17:53:54Z
- Repository state
- ativo
- Language
- Rust
- License
- Apache-2.0
- Added
- 2026-09-08 09:01:12
- Updated
- 2026-09-08 09:01:12
- Origin id
ImperialBower/cardpack.rs:.okf/index.md
README
# cardpack.rs
[](https://github.com/ImperialBower/cardpack.rs/actions/workflows/CI.yaml)
[](https://codecov.io/gh/ImperialBower/cardpack.rs)
[](https://crates.io/crates/cardpack)
[](https://docs.rs/cardpack/)
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](https://www.gnu.org/software/make/):
```shell
make
```
Run `make help` to see all available targets.
## Usage
```rust
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](https://en.wikipedia.org/wiki/French_playing_cards)
* [Pinochle](https://en.wikipedia.org/wiki/Pinochle#Deck)
* [Spades](https://en.wikipedia.org/wiki/Spades_(card_game)#General_overview) with [Jokers](https://en.wikipedia.org/wiki/Joker_(playing_card))
* [Standard 52](https://en.wikipedia.org/wiki/Standard_52-card_deck)
* [Canasta](https://en.wikipedia.org/wiki/Canasta#Cards_and_deal)
* [Hand and Foot](https://www.pagat.com/rummy/handfoot.html)
* [Euchre](https://en.wikipedia.org/wiki/Euchre)
* [Ganjifa](https://en.wikipedia.org/wiki/Ganjifa) with per-suit inverted pip ranking
* Mughal (8 suits × 12 = 96 cards)
* Dashavatara (10 suits × 12 = 120 cards)
* [Short Deck](https://en.wikipedia.org/wiki/Six-plus_hold_'em)
* [Skat](https://en.wikipedia.org/wiki/Skat_(card_game)#Deck)
* [Tarot](https://en.wikipedia.org/wiki/Tarot#Tarot_gaming_decks) with [Major](https://en.wikipedia.org/wiki/Major_Arcana) and [Minor](https://en.wikipedia.org/wiki/Minor_Arcana) Arcana
The project takes advantage of [Project Fluent](https://www.projectfluent.org/)'s
[Rust](https://github.com/projectfluent/fluent-rs) support to offer
internationalization. Current languages supported are
[English](src/localization/locales/en-US/french.ftl),
[German](src/localization/locales/de/french.ftl),
[French](src/localization/locales/fr/french.ftl),
[Latin](src/localization/locales/la/french.ftl), and
[Klingon](src/localization/locales/tlh/french.ftl).
## 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](#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](#funky--balatro-style-cards) below |
| `seal-test-double`| no | — | `PlaintextSeal` (**no security**) and the `seal_roundtrip` conformance helper for testing a `Seal` backend; **not** in `full` |
| `commit-reveal` | no | `sha2` | Provably-fair shuffles: `ShuffleRound`, `Commitment`/`Contribution`, `CombinedSeed`, `commit_pile`, `Pile::shuffled_by_round` — see [Provably-fair shuffles](#provably-fair-shuffles); **not** in `full` |
| `seal-aead` | no | `chacha20poly1305`, `hkdf`, `sha2`, `zeroize` | Holder-key seal: `HolderKeySeal`, `DealKey`/`CardKey`, `SealedBytes`, `Custody` — see [Sealed cards](#sealed-cards-holder-key-seal); **not** in `full` |
| `crypto` | no | = both above | Umbrella over `commit-reveal` + `seal-aead`; **not** in `full` |
To get the previous "batteries-included" behavior, opt into `full`:
```toml
# 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 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.
**Sealed Decks** ([EPIC-04](docs/EPIC-04_Sealed_Decks.md)) are always
on and dependency-free:
- `Ordinal`/`Codebook` (a canonical card ↔ number bijection per deck)
- `Permutation` (a shuffle as data)
- `SlotPile` (a shoe of card *names* that shuffles, cuts and deals with no knowledge)
- `Revealed` (the only slot → card map), and the five-item `Seal` adapter.
No kernel type holds ciphertext or is generic over a scheme. Real crypto backends are
planned as opt-in features outside `full`.
### Provably-fair shuffles
The `commit-reveal` feature ([EPIC-04a](docs/EPIC-04a_Commit_Reveal_Shuffle.md))
adds one dependency, `sha2`, and lets every participant in a game prove the
shuffle was fair. Each participant commits to secret entropy, then everyone
reveals; the combined seed fixes the shuffle through a frozen SHA-256
derivation that any verifier, in any language, can reproduce from the
public transcript alone:
```rust,ignore
// needs `--features commit-reveal`; the same example is a compiled doctest in `src/seal/commit/mod.rs`
use cardpack::prelude::*;
let (dealer, player) = (ParticipantId(1), ParticipantId(2));
let a = Contribution::from_bytes([0x11; 32]); // Contribution::random(&mut rng) in real code
let b = Contribution::from_bytes([0x22; 32]);
let mut round = ShuffleRound::new([dealer, player])?;
round.commit(dealer, a.commit())?;
round.commit(player, b.commit())?; // nobody may reveal before this point
round.reveal(dealer, a)?;
round.reveal(player, b)?;
let shuffled = Standard52::deck().shuffled_by_round(&round)?;
# Ok::<(), CardError>(())
```
`commit_pile` / `verify_pile` let a dealer publish a blind commitment to a
concrete deck order before dealing and opening it later. Run
`cargo ex provably_fair` for a two-party round end to end. This hides the
*shuffle*, not the *cards*. Hiding cards is the next feature.
### Sealed cards (holder-key seal)
The `seal-aead` feature ([EPIC-04b](docs/EPIC-04b_Holder_Key_Seal.md)) is the
first real `Seal` backend: a trusted dealer seals every card under its own
HKDF-derived key (XChaCha20-Poly1305, 42 public bytes per card), and a holder
turns one card up by publishing one 32-byte token. A spectator with no secret
verifies it through `Revealed::reveal_with`; the token opens nothing else.
```rust,ignore
// needs `--features seal-aead`; the same flow is a compiled doctest in `src/seal/aead/mod.rs`
use cardpack::prelude::*;
let dealer = HolderKeySeal::<Standard52>::dealer(DealKey::random(&mut rng), b"table-7/hand-12");
let (mut shoe, custody) = dealer.deal(&Standard52::deck(), &mut rng)?; // SlotPile + Custody
let hole = shoe.draw(2).unwrap(); // slot names, no values
let tokens = dealer.tokens_for(hole.slots().iter().copied())?;
// Holder publishes (slot, token); anyone verifies:
let spectator = HolderKeySeal::<Standard52>::verifier(b"table-7/hand-12");
let mut revealed = Revealed::<Standard52>::new();
let card = revealed.reveal_with(slot, custody.get(slot).unwrap(), &spectator, &token)?;
```
Three plain values — `SlotPile` (order), `Custody` (bytes), `Revealed`
(values) — and a scheme that lives inside none of them. The RNG you pass
**must** be a CSPRNG. Run `cargo ex holder_seal` for the flow end to end. The
`crypto` feature turns on both backends; none of them is in `full`.
## 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.
```rust,ignore
// 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](https://www.playbalatro.com/). 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](https://github.com/guyroyse/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](https://www.lua.org/), 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`](docs/EPIC-01_Funky.md).
There are two examples to see it in action:
```shell
# 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`](docs/wasm.md) for
the consumer-side `getrandom` backend setup, recommended feature
combos, and runtime gotchas. A working example lives at
[`examples/wasm.rs`](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](#cargo-features)), most examples need
`--features` to compile; `cargo ex` is an alias in
[`.cargo/config.toml`](.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:
```shell
❯ 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](https://en.wikipedia.org/wiki/Contract_bridge):
```shell
❯ 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](https://www.wikihow.com/Play-Hand-and-Foot).
- `cargo ex poker` - A random heads up [no-limit Poker](https://en.wikipedia.org/wiki/Texas_hold_%27em) deal.
- `cargo ex poker_eval` - Scores a Texas Hold'em board via the [`ckc-rs`](https://crates.io/crates/ckc-rs) evaluator, 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](#funky--balatro-style-cards)).
- `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). See [`docs/wasm.md`](docs/wasm.md).
## References
* [Card games in Germany](https://www.pagat.com/national/germany.html)
* [Playing cards in Unicode](https://en.wikipedia.org/wiki/Playing_cards_in_Unicode)
* [Balatro](https://www.playbalatro.com/)
* [balatrowiki.org](https://balatrowiki.org/)
* [balatrogame.fandom.com](https://balatrogame.fandom.com/)
* [Balatro Modding Guide](https://steamcommunity.com/sharedfiles/filedetails/?id=3400691352)
### Other Deck of Cards Libraries
* [ascclemens/cards](https://github.com/ascclemens/cards)
* [locka99/deckofcards-rs](https://github.com/locka99/deckofcards-rs)
* [vsupalov/cards-rs](https://github.com/vsupalov/cards-rs)
* [droundy/bridge-cards](https://github.com/droundy/bridge-cards)
* Tarot Libraries
* [lawreka/ascii-tarot](https://github.com/lawreka/ascii-tarot)
* [pietdaniel/tarot](https://github.com/pietdaniel/tarot)
* [jeremytarling/ruby-tarot](https://github.com/jeremytarling/ruby-tarot)
## Dependencies
* [Colored](https://github.com/colored-rs/colored)
* [Fluent Templates](https://github.com/XAMPPRocky/fluent-templates)
* [Project Fluent](https://www.projectfluent.org/)
* [itertools](https://github.com/rust-itertools/itertools)
* [log](https://github.com/rust-lang/log)
* [rand](https://github.com/rust-random/rand)
* [serde](https://github.com/serde-rs/serde)
* [serde_norway](https://crates.io/crates/serde_norway)
* [thiserror](https://github.com/dtolnay/thiserror)
## Dev Dependencies
* [term-table](https://github.com/RyanBluth/term-table-rs)
* [rstest](https://github.com/la10736/rstest) - Fixture-based test framework for Rust
## TODO
* [Hanafuda](https://en.wikipedia.org/wiki/Hanafuda)
* [고스톱 (Go-Stop)](https://en.wikipedia.org/wiki/Go-Stop)
* [Go-Stop - The Cards](https://www.sloperama.com/gostop/cards.html)
* [nbry/go-stop-rust](https://github.com/nbry/go-stop-rust)
* [Sakura](https://en.wikipedia.org/wiki/Sakura_(card_game))
* [Cinch](https://en.wikipedia.org/wiki/Cinch_(card_game))
* [Zwickern](https://en.wikipedia.org/wiki/Zwickern)
* [Beggar-my-neighbour](https://en.wikipedia.org/wiki/Beggar-my-neighbour)