Auditing Digital Dice True Randomness: Separating D&D Gameplay from PRNG Display Animations
Your Players Just Watched the Rogue Roll Three Natural Ones in a Row, and Now Nobody Trusts the VTT
Last Friday, you wrapped a four-hour session on Roll20. The encounter was balanced. The tactics were sound. But your rogue rolled three natural ones on consecutive Stealth checks, and by the second hour, the table had decided the platform was "cursed." One player pulled out physical dice. Another started screenshotting results. The session devolved into an argument about whether the digital d20 was rigged.
You've been here before. Maybe it was D&D Beyond's dice roller showing a suspicious cluster of low rolls during a boss fight. Maybe it was a Discord bot that seemed to favor certain numbers. The frustration is real, and it erodes the social contract at your table faster than any bad ruling you could make.
Here's the thing: your suspicion isn't crazy. But it's almost certainly aimed at the wrong layer of the software. The number your players see and the spinning animation that precedes it are two completely separate systems, and most arguments about "rigged digital dice" conflate them. This guide walks you through auditing the actual randomness of your platform's dice engine, separating cosmetic animation from mathematical output, and building a trust protocol your table can rely on.
The Core Problem: You're Auditing the Wrong Layer
When players complain about digital dice, they describe what they saw: a d20 tumbling across the screen, landing on a 1. Again. The animation feels weighted. The timing feels off. The visual feedback creates an emotional impression of bias.
But in virtually every modern digital dice platform, the random number is generated before the animation even begins. The animation is a playback layer. It receives a predetermined result and renders a visual sequence that ends on that number. The tumbling, the bouncing, the dramatic pause — none of it influences the outcome. It's theater.
This means when your players say "the dice feel rigged," they're reacting to a UI component, not the RNG engine. The actual randomness happens in a function call that completes in microseconds, invisible to everyone at the table.
The problem isn't that your platform is necessarily biased. The problem is that you have no way to verify it, because the layer you interact with is decoupled from the layer that matters.
Why This Specifically Matters for Online D&D DMs
As a Dungeon Master running online games, you occupy a unique position. You're the table's arbiter of fairness. When a player suspects the dice are rigged, they're implicitly asking you to adjudicate. But you can't adjudicate what you can't measure. Physical dice can be balanced with a saltwater test. Digital dice require a different audit — one that most DMs have never been trained to perform.
The Cause: How PRNG Actually Works in Browser-Based Dice Rollers
Nearly every browser-based dice platform — Roll20, D&D Beyond, Avrae, most Discord bots — relies on a Pseudorandom Number Generator, or PRNG. In JavaScript, this typically means Math.random(), which internally uses an algorithm like xorshift128+ or a similar deterministic function.
Here's what "deterministic" means in practice: given the same starting seed, the generator will always produce the same sequence of numbers. There's no atmospheric noise, no radioactive decay source, no quantum measurement. The generator takes a seed, runs it through a mathematical formula, and outputs a number that passes most statistical tests for randomness.
For a d20 roll, the platform generates a floating-point number between 0 and 1, then maps it to an integer range. The standard approach looks like Math.floor(Math.random() * 20) + 1. This produces integers from 1 to 20 with approximately equal probability.
The keyword is "approximately." PRNGs are designed for speed and statistical adequacy, not cryptographic security. They're good enough for gameplay, but they do have properties that differ from true randomness — and those properties become visible at scale.
The Display Animation Layer, Explained
Once the PRNG produces a result — say, a 4 — that number gets passed to the animation engine. The engine might use CSS transforms, WebGL, or a simple sprite swap. It renders a sequence of frames that visually "roll" and eventually settle on the 4 it was handed.
Some platforms add artificial delay. Roll20's 3D dice, for example, simulate physics: the die bounces, tumbles, and settles. This takes 1-3 seconds. During that time, the result has already been computed and is sitting in memory. The animation is purely for user experience.
This is why auditing the animation is pointless. You're measuring rendering performance, not randomness.
Step 1: Isolate the Number Generator from the Visual Layer
Your first audit step is to capture raw roll results before they reach the animation. This means intercepting the RNG output directly.
If you're using a browser-based platform, open your browser's Developer Tools (F12 in Chrome or Firefox). Navigate to the Console tab. Most dice platforms expose their roll results through a JavaScript event or a WebSocket message. You're looking for the raw integer, not the rendered frame.
For Roll20 specifically, roll results appear in the chat log as text. You can scrape these programmatically. For D&D Beyond, the roll result is included in the page's data model before the animation plays. For Avrae (the Discord bot), results are returned in the message payload — the animation is a separate emoji reaction.
The goal here is simple: build a dataset of raw integers. No animations. No visual bias. Just numbers.
What to Capture
Record three things for each roll: the timestamp, the die type (d20, d6, etc.), and the raw result. Store these in a spreadsheet. You'll need at least 500 rolls for a preliminary analysis, though 1,000 is better. We'll discuss why in the next step.
Step 2: Build a Sample Size That Actually Means Something
Here's where most amateur audits fail. A player rolls three natural ones in a session and declares the platform rigged. Three rolls tell you nothing. The probability of rolling three consecutive 1s on a fair d20 is (1/20)³, or 1 in 8,000. That's unlikely, but across millions of sessions played weekly, it will happen to hundreds of tables. It's not evidence of bias. It's statistics.
To detect actual bias, you need a sample large enough that random clustering doesn't dominate your signal. For a d20, the chi-square goodness-of-fit test — which we'll use in Step 3 — requires an expected frequency of at least 5 per category. With 20 possible outcomes, that means a minimum of 100 rolls. But 100 rolls give you very weak statistical power. At that sample size, you'd only detect gross bias.
A practical minimum: 600 rolls. This gives you an expected 30 occurrences per face on a d20. Better yet, 1,000 rolls, yielding 50 per face. At 1,000 rolls, the chi-square test can detect deviations of roughly 5-7% from expected frequency with reasonable confidence.
How to Collect 1,000 Rolls Without Losing Your Mind
Don't collect these during actual gameplay. Your players will revolt. Instead, use the platform's roll command in a private session. Most platforms support a macro or API call that lets you roll quickly. On Roll20, you can type /roll 1d20 in the chat. On Avrae, use !roll 1d20. Set up a simple script or browser extension to automate the process and log results.
A concrete example: I ran 1,000 automated d20 rolls through Avrae's Discord bot using a Python script with the discord.py library. The script sent !roll 1d20 every 2 seconds (to avoid rate limiting) and parsed the bot's response. Total collection time: about 33 minutes. The resulting dataset had 1,000 integers ranging from 1 to 20.
Step 3: Run the Chi-Square Test on Your Collected Data
Now you have 1,000 rolls. Time to test whether they're actually uniform.
The chi-square goodness-of-fit test compares your observed frequencies against the frequencies you'd expect from a fair die. For a d20 with 1,000 rolls, each face should appear 50 times (1,000 ÷ 20 = 50).
Here's the calculation. Count how many times each face appeared. Let's say your dataset looks something like this: 1 appeared 47 times, 2 appeared 53 times, 3 appeared 49 times, and so on. For each face, compute:
(observed - expected)² / expected
Sum these values across all 20 faces. That sum is your chi-square statistic.
With 20 categories, your degrees of freedom is 19 (categories minus 1). The critical value for significance at p = 0.05 is 30.14. If your chi-square statistic exceeds 30.14, you have evidence of non-uniform distribution. If it's below 30.14, your data is consistent with a fair die.
In the Avrae dataset I collected, the chi-square statistic was 22.8. Well below the critical threshold. The distribution wasn't perfectly flat — 14 appeared 58 times, 7 appeared 43 times — but these deviations were within the range expected from normal random variation.
Interpreting Results Without Overcorrecting
A passing chi-square test doesn't prove true randomness. It proves your platform's PRNG produces a uniform distribution across 1,000 rolls, which is what you actually need for D&D gameplay. Don't chase cryptographic-grade randomness for a tabletop game. You need statistical fairness, not quantum indeterminacy.
Step 4: Detect Seed Resets and Session-Based Patterns
Uniform distribution across 1,000 rolls is necessary but not sufficient. A PRNG can produce a uniform marginal distribution while still exhibiting sequential patterns — and those patterns are what players actually notice during gameplay.
Here's the concern: some browser-based platforms reseed their PRNG on page load. If the seed is derived from a low-entropy source like Date.now(), and multiple players load the page at similar times, their first few rolls could share sequential similarities. This is rare in well-engineered platforms, but it's worth checking.
To test for sequential patterns, compute the autocorrelation of your roll sequence. Specifically, check whether roll N predicts roll N+1 beyond chance. For a fair d20, the correlation between consecutive rolls should be approximately zero.
A simpler heuristic: split your 1,000-roll dataset into two halves. Run the chi-square test on each half separately. If both halves pass individually, and the means of both halves are within 1.0 of each other (on a 1-20 scale, that means both halves average between 10.0 and 11.0), you've ruled out most session-based drift.
Step 5: Translate Your Findings into a Table Trust Protocol
You've done the audit. Now you need to communicate the results to your players in a way that rebuilds trust without requiring them to understand chi-square mathematics.
Here's a protocol that works:
First, share the raw data. Post the spreadsheet link in your Discord server. Transparency matters more than explanation. Players who want to verify can run their own tests.
Second, present the summary in plain language. "I rolled 1,000 d20s through the platform. The results are consistent with a fair die. Here's the test." Link to the chi-square calculation. Don't overstate — say "consistent with fair," not "proven random."
Third, establish a policy for streaks. Decide as a table how you'll handle suspicious roll sequences. Some groups allow a reroll after three consecutive natural ones. Others accept the rolls as-is. The policy matters less than the fact that it exists and was agreed upon before the next streak happens.
Fourth, revisit the audit quarterly. Platforms update their code. A PRNG that was fair in January might be replaced in a June update. Set a calendar reminder to rerun the 1,000-roll test every three months.
When to Switch Platforms: Recognizing Unfixable Bias
Most platforms will pass this audit. PRNGs are well-understood, and mainstream dice rollers use standard implementations. But if your chi-square test fails — if your statistic exceeds 30.14 on a 1,000-roll d20 sample — don't immediately assume malice. Rerun the test with a fresh 1,000 rolls. If it fails again, you've identified a real problem.
At that point, switching platforms is the correct response. But switch intelligently. Before adopting a new platform, run the same 1,000-roll audit on it. Don't trust marketing claims about "true randomness" or "quantum dice." Trust the data.
One note on "true random" platforms: some services, like random.org, use atmospheric noise as their entropy source. This is genuinely non-deterministic. But for D&D gameplay, the difference between a good PRNG and true randomness is statistically undetectable at the sample sizes your table will ever generate. A well-seeded PRNG is sufficient. Don't pay a premium for quantum randomness to resolve an argument about a barbarian's attack roll.
The workable outcome you were promised: by following these five steps, you can walk into your next session with a dataset, a statistical test, and a table-level protocol that replaces vibes-based suspicion with evidence-based confidence. Your players may still roll three natural ones in a row. But now you'll know whether to blame the platform — or just the dice gods.