| name | dice-authenticity-standards |
| description | Guide and workflow for Dice Roll Authenticity Standards. Use when you need Dice Roll Authenticity Standards. |
Dice Roll Authenticity Standards
Overview
This document defines the standards for verifying dice roll authenticity in WorldArchitect.AI. It covers statistical validation (chi-squared testing) and code-level RNG verification.
The Fabrication Problem
LLMs can "fabricate" dice rolls by outputting dice values without using actual random number generation:
print('{"rolls": [16], "total": 21}')
import random
roll = random.randint(1, 20)
print(f'{{"rolls": [{roll}], "total": {roll + 5}}}')
Chi-Squared Test
What It Is
A statistical test measuring whether observed dice rolls match expected random distribution.
Formula
χ² = Σ [(observed - expected)² / expected]
For a d20 with N total rolls:
- Expected frequency per face: N / 20
- Compare observed counts for each face (1-20)
Interpretation Thresholds
| Chi-Squared Value | Interpretation | Action |
|---|
| 0-19 | Excellent - Highly uniform | Pass |
| 19-30 | Normal - Expected random variation | Pass |
| 30-50 | Suspicious - Minor anomaly | Investigate |
| 50-100 | Concerning - Significant deviation | Flag for review |
| 100-200 | Very unlikely from true RNG | Likely fabrication |
| 200+ | Statistically impossible | Confirmed fabrication |
| 411.81 | Reference: PR #2551 bug | Known fabrication case |
Sample Size Requirements
| Die Type | Minimum Rolls | Recommended |
|---|
| d4 | 40 | 100+ |
| d6 | 60 | 150+ |
| d20 | 200 | 500+ |
Chi-squared is unreliable with small sample sizes.
Python Implementation
from scipy import stats
import numpy as np
def chi_squared_test(rolls: list[int], die_size: int = 20) -> dict:
"""
Test dice roll distribution for uniformity.
Returns:
dict with chi2 value, p_value, and verdict
"""
observed = np.zeros(die_size)
for roll in rolls:
if 1 <= roll <= die_size:
observed[roll - 1] += 1
expected = len(rolls) / die_size
expected_array = np.full(die_size, expected)
chi2, p_value = stats.chisquare(observed, expected_array)
if chi2 < 30:
verdict = "PASS - Normal random variation"
elif chi2 < 50:
verdict = "WARNING - Minor anomaly"
elif chi2 < 100:
verdict = "FAIL - Significant deviation"
else:
verdict = "FAIL - Likely fabrication"
return {
"chi_squared": chi2,
"p_value": p_value,
"sample_size": len(rolls),
"verdict": verdict,
"distribution": dict(zip(range(1, die_size + 1), observed.astype(int).tolist()))
}
RNG Verification (Code-Level)
The Problem with Substring Matching
Old approach (vulnerable):
def _code_contains_rng(code_text: str) -> bool:
return "random.randint" in code_text
AST-Based Detection (Current Standard)
The fix uses Abstract Syntax Tree parsing to detect actual function calls:
import ast
def _code_contains_rng(code_text: str) -> bool:
"""Detect actual RNG function calls using AST parsing."""
tree = ast.parse(code_text)
for node in ast.walk(tree):
if isinstance(node, ast.Call):
target = _get_call_target(node.func)
if target in RNG_PATTERNS:
return True
return False
Verified RNG Patterns
| Module | Functions |
|---|
random | randint, choice, random, uniform, randrange, sample, shuffle |
secrets | randbelow, choice |
numpy.random | randint, choice, random, uniform, integers, permutation, randrange, sample, shuffle |
Evidence Fields
The extract_code_execution_evidence() function returns:
| Field | Type | Meaning |
|---|
code_execution_used | bool | Code was executed |
code_contains_rng | bool | RNG function found in code |
rng_verified | bool | code_execution_used AND code_contains_rng |
stdout_is_valid_json | bool | Output is valid JSON |
Fabrication Detection Logic
def is_fabrication(evidence: dict) -> bool:
"""
Fabrication = dice values present but no verified RNG.
"""
if evidence.get("rng_verified", False):
return False
if evidence.get("code_execution_used", False):
return True
return False
Testing Standards
Unit Test Requirements
- RED test: Must fail without the fix
- GREEN test: Must pass with the fix
- Regression protection: Removal of fix causes test failure
Chi-Squared Test Coverage
class TestDiceDistribution(unittest.TestCase):
def test_chi_squared_normal_distribution(self):
"""Verify true RNG produces acceptable chi-squared."""
rolls = [random.randint(1, 20) for _ in range(500)]
result = chi_squared_test(rolls, 20)
self.assertLess(result["chi_squared"], 50)
def test_chi_squared_detects_fabrication(self):
"""Verify fabricated dice fail chi-squared."""
fabricated = [16] * 100
result = chi_squared_test(fabricated, 20)
self.assertGreater(result["chi_squared"], 100)
RNG Verification Test Coverage
class TestRNGVerification(unittest.TestCase):
def test_detects_real_rng(self):
code = "roll = random.randint(1, 20)"
self.assertTrue(_code_contains_rng(code))
def test_rejects_string_containing_rng(self):
code = "print('random.randint is cool')"
self.assertFalse(_code_contains_rng(code))
def test_rejects_fabricated_print(self):
code = 'print(\'{"rolls": [16]}\')'
self.assertFalse(_code_contains_rng(code))
Audit Workflow
1. Statistical Analysis (Chi-Squared)
WORLDAI_DEV_MODE=true python scripts/audit_dice_rolls.py <campaign_id>
Look for:
- Chi-squared value in output
- Distribution skew warnings
- Impossible values (0, 21+ on d20)
2. Code-Level Verification
Check GCP logs for:
DICE_AUDIT: ... rng_verified=True
CODE_EXEC_NO_RNG: ... # Warning - fabrication detected
3. Response to Fabrication
If chi-squared > 100 or rng_verified=False:
- Immediate: Reprompt LLM with enforcement warning
- Investigation: Review code_execution samples in logs
- Fix: Ensure AST-based RNG detection is active
Related Documentation
dice-roll-audit.md - Campaign analysis workflow
dice-real-mode-tests.md - MCP test procedures
evidence-standards.md - Three-evidence rule
Reference: PR #2551
The chi-squared test and AST-based RNG verification were implemented in PR #2551 to fix dice fabrication:
- Bug: Chi-squared 411.81 (vs expected 19-30)
- Cause: LLM printed dice values without calling
random.randint()
- Fix: AST parsing +
rng_verified field + enforcement warning in system prompt