How Our Dice Work

We Take Our Dice
VERY Seriously.
Mathematics at its purest.

The Artificer's finest enchantment ensures no mortal hand tips the scales. Your fate belongs to the mathematics of the cosmos. Also, your physical d20 is lying to you.

Your d20 has a secret

We love physical dice. The weight, the clatter, that moment of suspense as the die settles. But here's the uncomfortable truth that dice manufacturers would rather you not think about: your shiny math rocks are not perfectly fair.

Sources of physical dice bias:

  • 1.That gate mark on your d20? Not a beauty spot. It is a thumb on the scale. Injection moulding leaves a tiny dense spot where plastic was forced in. Independent testing (Daniel Fisher, Awesome Dice Blog) has shown measurable bias in standard Chessex d20s.
  • 2.Paint: the silent saboteur. The number 18 has more carved surface area than 1, which means more paint, which means more weight on that face. Every digit is a different mass. Your d20 is lopsided by design.
  • 3.Rounded edges are not your friend. Cheap dice tumble like drunken halflings. Uneven rounding means certain faces become favourite resting spots. Your die has preferences, and it did not consult you.
  • 4.Your "lucky technique" is just physics you do not control. Felt trays, wooden tables, the height of the drop, the spin of your wrist. Your "ritual shake" is producing the same trajectories over and over. Consistency is the enemy of randomness.
  • 5.Air bubbles hiding inside your dice, silently rigging your death saves since 2004. Opaque dice can conceal internal air pockets that shift weight distribution. You cannot see them. You cannot fix them. They do not care about your backstory.

None of this means physical dice are bad. They are a ritual, a tradition, a sensory experience. But if you want true mathematical fairness, you need something better.

How we roll (literally)

Every single dice roll in ArchitectRPG, from initiative to damage to death saves, uses the Web Crypto API's crypto.getRandomValues() function. This is a cryptographically secure pseudorandom number generator (CSPRNG) backed by your operating system's hardware entropy pool.

1

Hardware entropy collection

Your CPU collects genuine physical randomness from thermal noise, electronic jitter, and other hardware sources. Intel chips use RDRAND/RDSEED instructions. ARM chips use similar hardware RNG modules. This is real entropy, not a formula pretending to be random.

2

CSPRNG expansion

The OS kernel feeds this hardware entropy into a CSPRNG (ChaCha20 on Linux, BCryptGenRandom on Windows, Fortuna on macOS). This expands the entropy into a stream of unpredictable bytes. Even if you observed a billion previous rolls, you could not predict the next one.

3

Rejection sampling (the clever bit)

Here's where most implementations get lazy. A naive approach would do randomNumber % 20 to get a d20 roll. But 232 (4,294,967,296) does not divide evenly by 20. The remainder means some outcomes are slightly more likely than others. We use rejection sampling to eliminate this bias entirely.

combat-engine.tsTypeScript
function cryptoRandomInt(min: number, max: number): number {
  const range = max - min + 1;
  // Calculate the largest multiple of range that fits in 32 bits
  const maxValid = Math.floor(0xFFFFFFFF / range) * range;
  const arr = new Uint32Array(1);
  let val: number;
  do {
    // Draw from hardware entropy via Web Crypto API
    crypto.getRandomValues(arr);
    val = arr[0];
    // Reject values that would cause modulo bias
  } while (val >= maxValid);
  return min + (val % range);
}

If the random value falls in the biased "overflow" zone at the top of the 32-bit range, we throw it away and draw again. This guarantees perfectly uniform distribution across all die faces. The rejection rate is negligible (less than 0.0000005% for a d20).

4

Result-before-animation

The result is locked in before ANY animation begins. Your client generates the number, logs it to an immutable provenance chain, then tells the 3D renderer which face to land on. The pretty animation is just theatre. The maths was already done. The AI narrator receives the result as a fait accompli and describes what the universe already decided.

The wall between dice and narrative

We call it the "Iron Rules, Silk Tongue" architecture. The rules engine is iron: deterministic, auditable, incorruptible. The AI narrator is silk: creative, dramatic, evocative. They never cross. The AI cannot influence dice outcomes any more than a sports commentator can change the score.

Here's what happens in the 200 milliseconds between "I attack the troll" and "Your blade finds its mark":

Player"I swing my greatsword at the troll"
Game Orchestratorclassifies action asATTACK
Combat Enginerollscrypto.getRandomValues()d20 = 17
Rules Enginecomputes17 + 7 = 24 vs AC 15 =HIT
Combat Enginerolls damage2d6 = [4, 5] + 4 STR =13 slashing
AI Narrator"Steel meets flesh. The greatsword bites deep into the troll's shoulder..."

The AI receives the mechanical result (hit, 13 damage) as a fait accompli. It cannot re-roll, fudge, or "adjust for drama." If you roll a natural 1 on a death save, the narrator will describe it beautifully, but that character is still in trouble.

The brutally honest comparison

We are not going to pretend digital dice are better in every way. Physical dice have something we will never replicate. But on the maths? We win. Decisively.

ArchitectRPGPhysical Dice
Entropy SourceHardware CSPRNGWrist flick physics
BiasNone (rejection sampling)Manufacturing defects, wear, surface
AuditabilityEvery roll logged with provenanceTrust the table
DistributionPerfectly uniformApproximately uniform
SpeedNanosecondsSeconds (plus the hunt under the sofa)
Satisfying clackNo. We admit it.Absolutely glorious
That ritual feelingClick a button. We know.Shake, blow on them, whisper threats

2.4M+

Dice rolled in beta

0

Dice fudged

100%

Provably fair

Every Roll Has a Receipt

Every dice roll generates a cryptographic provenance record with a SHA-256 hash chain. Each roll is linked to the previous one, creating an unbreakable audit trail. The GM cannot alter results after they are generated. You can verify the entire chain at any time. Because trust should be provable, not assumed.

Three nat 1s in a row and you think we hate you

We hear this. You rolled three natural 1s in a row and you are convinced the system hates you. Here is why that feeling is perfectly normal and mathematically expected:

Humans are terrible at recognising randomness

Studies consistently show that when asked to generate "random" sequences, humans avoid repeats and streaks. Real randomness is full of them. Three 1s in a row on a d20 has a 1 in 8,000 chance per sequence of three rolls. Over a 4-hour session with 50+ rolls, streaks are not just possible, they are expected. If you never saw a streak, that would actually be evidence of a non-random system.

Confirmation bias is a hell of a drug

You remember the natural 1 on the critical death save. You do not remember the 14 perfectly average rolls before it. Your brain is wired to notice patterns and assign meaning to coincidence. A truly random system will occasionally produce sequences that look suspicious to a human observer. That is a feature, not a bug.

Physical dice have trained you wrong

If your physical d20 has been slightly biased toward middle values (13-17) due to manufacturing imperfections, you have spent years calibrating your "what random feels like" against a non-uniform distribution. When you switch to a perfectly uniform system, the extremes (1-4 and 17-20) will seem to appear "too often." They are not. They are appearing at exactly the right frequency for the first time.

Receipts. We keep receipts.

Every dice roll in every game session is logged with full provenance. Purpose, die type, raw result, modifier, total, whether it was a critical. No roll is ever lost, hidden, or retroactively changed. Your combat log is a complete, auditable record of every mechanical decision the system made.

DiceRollRecordTypeScript
interface DiceRollRecord {
  purpose: string;        // "Brynn attacks Troll with Greatsword"
  dieType: string;        // "d20"
  result: number;         // 17 (raw roll, untouched)
  modifier: number;       // +7 (STR mod + proficiency)
  total: number;          // 24 (result + modifier)
  critical: "success"     // Natural 20
           | "fuckup"     // Natural 1
           | null;        // Normal roll
}

Why Math.random() gets an F

For the technically curious: JavaScript's Math.random() uses the xorshift128+ algorithm in V8 (Chrome/Node.js). It is fast and statistically decent, but it has properties that make it unsuitable for dice:

  • xPredictable. Given enough output, the internal state can be reconstructed and future values predicted. A sufficiently motivated player could theoretically predict dice rolls.
  • xNot uniformly distributed at fine granularity. It produces 64-bit floats, and the mapping to integers introduces subtle biases depending on the range.
  • xSeeded from a single timestamp. Two servers starting at similar times could produce correlated sequences.

crypto.getRandomValues() has none of these problems. It is backed by hardware entropy, resistant to prediction, and uniformly distributed by design. It is the same API used by TLS, password generators, and cryptographic key generation. If it is good enough to protect your bank account, it is good enough for your attack roll.

Enough talk. Roll something.

Do not take our word for it. Roll a few hundred times and see for yourself. Every result below is generated using the exact same code path as a live game session.

Try It Yourself

Every roll uses crypto.getRandomValues() with rejection sampling. The 3D animation is reverse-rigged to land on the pre-determined result.

Dice Pool (click dice above to add, then Roll All)

Loading dice...

Your Active Dice Set

Adventurer's Stone

Common - Pristine

Customise your dice in Settings or browse the Marketplace.

The bottom line

We cannot replicate the satisfying clatter of dice on a table. But we can guarantee that every roll is perfectly, verifiably, mathematically fair.

Your dice are enchanted. Your fate is sealed. The only question left is whether you are brave enough to roll.

Roll Initiative. For Real This Time.

Want to verify our implementation? The source code for our dice engine is in combat-engine.ts. We believe in transparency, not trust.