How Our Dice Work
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.
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.
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.
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.
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.
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.
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.
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).
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.
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":
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.
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.
| ArchitectRPG | Physical Dice | |
|---|---|---|
| Entropy Source | Hardware CSPRNG | Wrist flick physics |
| Bias | None (rejection sampling) | Manufacturing defects, wear, surface |
| Auditability | Every roll logged with provenance | Trust the table |
| Distribution | Perfectly uniform | Approximately uniform |
| Speed | Nanoseconds | Seconds (plus the hunt under the sofa) |
| Satisfying clack | No. We admit it. | Absolutely glorious |
| That ritual feeling | Click a button. We know. | Shake, blow on them, whisper threats |
2.4M+
Dice rolled in beta
0
Dice fudged
100%
Provably fair
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.
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:
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.
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.
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.
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.
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
}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:
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.
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.
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)
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.
No rolls recorded yet. Roll some dice to populate the provenance chain.