Back to the catalog

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

[![Build and Test](https://github.com/ImperialBower/cardpack.rs/actions/workflows/CI.yaml/badge.svg)](https://github.com/ImperialBower/cardpack.rs/actions/workflows/CI.yaml)
[![codecov](https://codecov.io/gh/ImperialBower/cardpack.rs/branch/main/graph/badge.svg)](https://codecov.io/gh/ImperialBower/cardpack.rs)
[![Crates.io Version](https://img.shields.io/crates/v/cardpack.svg)](https://crates.io/crates/cardpack)
[![Rustdocs](https://docs.rs/cardpack/badge.svg)](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)

More