Skip to content

Randomness → unbiased d6 (PvP games)

Map PvP randomness to six-sided dice without byte-level modulo bias. Keep on-chain rolls and off-chain previews in sync through the same sampling rules.

Audience: Agents and humans implementing IPvpGameV2.onRandomness or off-chain mirrors (mocks, tests, guest previews).

Applies to: Any PvP game that maps facet / VRF bytes32 randomness to six-sided dice (faces 1..6). Examples: dice poker deals, Yahtzee-style re-rolls, any onPlayerAction → WAITING_RANDOMNESS → onRandomness loop.

The byte-level algorithm is identical to @chain-protocol/casino-sdk RANDOMNESS_DICE. This file is duplicated here so PvP integrators do not need to read the casino package.


Never map a uniform byte to 1..6 with (byte % 6) + 1 alone.

Use rejection sampling: DIE_REJECT = 252, accept only byte < 252, then face = (byte % 6) + 1.

See the casino-sdk copy for the bias table, Solidity/TypeScript reference implementations, agent checklist, and general limit = floor(M/n)*n guidance.


  1. onPlayerAction (or onLobbyStart) may return requestRandomnessNow = true → WAITING_RANDOMNESS.
  2. Facet requests RNG; provider fulfills.
  3. onRandomness(ctx, bytes32 randomness) maps bytes to dice/outcomes, updates newGameState, may return to WAITING_PLAYER_ACTION (same player can act again) or advance resolution.

Turn order lives in gameState; the facet only forwards msg.sender — your game validates the actor after consuming randomness.


Many dice from one word (alternative pattern)

Section titled “Many dice from one word (alternative pattern)”

A game that needs many independent dice can derive each die as:

uint256 word = uint256(keccak256(abi.encode(randomness, player, index)));
return uint8((word % DIE_FACES) + 1);

Modulo bias on 2²⁵⁶ is negligible. That pattern is fine when you expand entropy per die with keccak256(abi.encode(...)).

It is not a substitute for rejection when you read raw bytes directly (randomness[i] % 6) — agents often make that mistake when porting casino byte-walk code into PvP.

PatternWhen to use
Byte walk + byte < 252 (this doc)Sequential consumption of VRF bytes, 2d6, shared cursor across multiple dice
keccak256(abi.encode(randomness, salt…)) then word % 6Many independent dice from one fulfillment; salts must be unique per die

Pick one style per game and use it consistently in contract, mocks, and guest simulation.


DODON’T
Same rejection rules as casino when using raw bytesAssume PvP “doesn’t matter” because it’s not house edge
Unique abi.encode salts per die in keccak-expanded rollsReuse the same salt for two dice in one roll
Mirror contract logic in guest mocks before showing outcomesMath.random() for pot resolution
Keep contract, test, and guest die helpers in syncMix byte-walk for deal and raw % 6 on bytes for re-roll