Outcome formulas
Five patterns cover almost every VRF application. Pick one, paste it into your dice_callback, change the constants.
Two rules
Before the formulas, two things that apply to all of them.
1 · Always use u32::from_le_bytes / u64::from_le_bytes for larger reductions. Reading bytes one-at-a-time and folding them yourself is an easy way to introduce modulo bias. The formulas below use the first 4 or 8 bytes as a single little-endian integer before taking the modulus.
2 · 32 bytes is a lot of randomness. Don't feel like you have to reuse the whole payload for one decision. If you need multiple random choices per round (e.g. deal 5 cards), use disjoint byte windows — bytes 0..4 for card 1, 4..8 for card 2, and so on.
Dice · 1 to 6
The canonical example. A u32 modulo 6, plus 1.
// 1 to 6, uniform, biased by at most 2^-29 — safe for any game.
let r = result.randomness;
let roll = (u32::from_le_bytes([r[0], r[1], r[2], r[3]]) % 6) as u8 + 1;ℹ What about modulo bias?
A u32 has 2^32 possible values. 6 does not divide 2^32 evenly — four values map to {1,2,3,4} but only two map to {5,6}, off by a factor of roughly 2^-29. No human game cares at that level. If you need stronger uniformity (e.g. for a cryptographic protocol), use rejection sampling instead.
Coin flip
The cleanest one — a single bit is exactly uniform.
// 0 or 1 — exactly uniform because we use a single bit.
let r = result.randomness;
let side: u8 = r[0] & 1;
// or if you want heads/tails labels:
let heads = (r[0] & 1) == 0;Lottery winner
Pick one of n entrants. Use u64 so the pool is large enough that modulo bias stays negligible for any practical n.
// Pick one of n_players. Uniform modulo bias is 2^-57 for n ≤ 2^10,
// which is invisible to any human-scale lottery.
let r = result.randomness;
let pool: u64 = u64::from_le_bytes([
r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7],
]);
let winner_index: usize = (pool % n_players as u64) as usize;Weighted wheel
Loot drops, tier distributions, prize tables. Each segment has an integer weight and the sum is total. Walk the segments until the accumulated weight exceeds the draw.
// Weighted segments that sum to `total`. Walk until the accumulator
// passes the draw. Works for any integer weights.
let r = result.randomness;
let draw = u32::from_le_bytes([r[0], r[1], r[2], r[3]]) % total;
let mut acc = 0u32;
let mut picked: usize = 0;
for (i, w) in segment_weights.iter().enumerate() {
acc = acc.saturating_add(*w);
if draw < acc {
picked = i;
break;
}
}Concrete example
Suppose you're rolling loot with weights [60, 30, 9, 1] for common/uncommon/rare/legendary. Sum is 100. A draw = 87lands in the third segment (60 + 30 = 90 > 87 after the second iteration) → picks index 2, the "rare" bucket.
Threshold binary
Yes/no decisions with controllable probability. For 50/50, compare to u64::MAX / 2. For arbitrary probabilities, compute p_as_u64 = (p * u64::MAX as f64) as u64 off-chain and pass it in.
// A fair coin, but with higher resolution (useful if you want to support
// biased thresholds like "yes with 37% probability" in the future).
let r = result.randomness;
let draw = u64::from_le_bytes([
r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7],
]);
let yes = draw > (u64::MAX / 2);
// For a biased threshold p (as a fraction of u64::MAX):
let yes_biased = draw < p_as_u64;Reusing bytes across decisions
If a single round has to make several independent decisions (e.g. "pick a card, then pick a modifier, then decide if it crits"), split the 32 bytes into disjoint windows. This guarantees the decisions are mutually independent.
// Card (6-range) + modifier (4-range) + crit (50/50), all independent.
let r = result.randomness;
let card = (u32::from_le_bytes([r[0], r[1], r[2], r[3]]) % 6) as u8;
let mod_ = (u32::from_le_bytes([r[4], r[5], r[6], r[7]]) % 4) as u8;
let crit = u64::from_le_bytes([r[8], r[9], r[10], r[11],
r[12], r[13], r[14], r[15]]) > u64::MAX / 2;✓ Budget check
32 bytes = 256 bits. That's 32 independent 1/256 decisions or 8 independent 1-in-a-billion choices. Almost every game fits comfortably within a single round's payload.