bitdeck is a fixed-capacity (N ≤ 128) Bitmask. [Deck<N>] stores subset membership as a bitmask — u64 by default (up to 64 cards), or u128 (up to 128 cards) — enabling deterministic subset queries, bulk mutations, and (with the rand feature) uniform random draws without replacement and non-destructive peeks.
While it includes a standard 54-card deck preset ([Standard]) behind the cards feature, [Deck<N>] is completely generic and can be used for loot tables, turn based action queues, shuffle bags etc.
rand(default): enables the random draw/peek APIs, including the*_maskbulk helpers.alloc(default): enables the draw/peek*_intohelpers that fill analloc::vec::Vec. Requiresrand.serde: transparent bitmask serialization for [Deck<N>] (the backing mask typeMis serialized transparently).bevy: derivesComponentandReflectfor [Deck<N>], so it can be attached to entities, or wrapped in aResource.cards: exposes thecardsmodule with the [Standard] deck newtype and its typed subsets — suits, ranks, colors, jokers, and predefined subsets like [Color::Red] and [FaceCards].
The crate is no_std. The default feature set includes alloc; disable default features and enable only the features you need for a no_std environment without an allocator.
- Uniform without replacement. Every remaining item is equally likely on every draw; drawn items leave the deck. Use multiple copies of each item for weighted randomness.
- Subset-aware. Draw from or query any subset (eg: a heart, a red card, a common drop) with a plain bitmask of type
M, or use typed subsets scoped to a specific deck. - Typed subsets. The [
deck!] macro generates a deck newtype together with classification enums and fixed unit-struct subsets that implement [Subset]. PassSuit::HeartsorJokersdirectly todraw_subset,count_subset, etc. - Const mask algebra. Classification enums expose const
mask(),from_id(), andALL; compose them with|,&, and!in const contexts. - Bring your own RNG. All randomness comes from a caller-supplied
randRNG; the deck itself holds no RNG state.
Define a deck and its classification enums together with [deck!].
Each variant maps to a bitmask, and the generated newtype accepts any
impl Subset<_> in its *_subset methods:
use bitdeck::{deck, Deck};
use rand::{SeedableRng, rngs::SmallRng};
deck! {
struct Loot = Deck<6>;
subsets {
enum Rarity { Common, Rare }
from_id = |id: u8| id / 3;
cards = 6;
}
}
let mut rng = SmallRng::seed_from_u64(42);
let mut pool = Loot::default();
let rare = pool.draw_subset(&mut rng, Rarity::Rare).unwrap();
assert!(rare >= 3);
assert_eq!(pool.count_subset(Rarity::Common), 3);Raw bitmask APIs (typed by the deck's mask type M) are still available through Deref.
[Deck<N>] is just a bitmask of type M (u64 by default, u128 for larger decks).
Every operation below is a thin wrapper around a bitwise read or mutation on
that mask.
- Bitwise subset queries and bulk mutations are O(1).
- Random draws/peeks are uniform without replacement. Selecting a card is O(1)
on
x86_64with BMI2 — foru64andu128masks alike — and O(log 64) with the portable fallback elsewhere. See Randomness for the RNG values each bulk helper consumes.
POPCNT and BMI2 are detected via CPUID on first use and used when
available, so no flags are needed on capable CPUs. To skip detection and
require the instructions at runtime, enable them at compile time:
RUSTFLAGS="-C target-feature=+bmi2,+popcnt" cargo buildNote that this produces a binary that requires POPCNT and BMI2 at runtime;
the default portable build uses them only when the CPU advertises support.
Draws and peeks are uniform without replacement and consume only the RNG you pass in. A given RNG state and call sequence always produce the same result; what varies between helpers is how many RNG values a bulk call consumes.
The bulk helpers ([Deck::draw_mask], [Deck::draw_in_mask],
[Deck::peek_mask], [Deck::peek_in_mask], and their *_into variants)
consume values as follows, where available is the number of selected cards
still in the deck:
Requested count |
RNG values consumed |
|---|---|
count == 0 |
none |
0 < count <= available / 2 |
count |
available / 2 < count < available |
available - count (the complement is sampled) |
count >= available |
none (every available card is returned) |
The returned mask is always a uniform random subset of size
min(count, available), so only the RNG position differs: for
count > available / 2 a seeded run does not line up with a naive "draw count
times" loop, and later draws from the same RNG follow from that position.
Single-card [Deck::draw], [Deck::draw_in], [Deck::peek], and
[Deck::peek_in] always consume exactly one value, and non-random operations
consume none.