⚙️ Core Entropy Generator (`main.c`)
The core program reads high-entropy hardware random data from /dev/urandom and applies a modulo-bias elimination technique (simple discard method) to ensure uniformly distributed rolls across standard RPG dice types (D4 to D100).
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <unistd.h>
// Dice list
typedef struct {
int n;
enum {
D4 = 4,
D6 = 6,
D8 = 8,
D10 = 10,
D12 = 12,
D20 = 20,
D100 = 100
} dices_type;
} Dices;
// specifics about the code inside README and /include/src.h
// Why the function is normally distributed explained inside the README
// Inside /include/src.h is contained the Enum explanation
uint8_t simple_discard_method(int divisor, FILE *rand_reader) {
int limit = (256 / divisor) * divisor;
uint8_t random_byte;
do {
fread(&random_byte, sizeof(uint8_t), 1, rand_reader);
} while (random_byte >= limit);
int result = (random_byte % divisor) + 1;
return result;
}
int main() {
FILE *rand_reader = fopen("/dev/urandom", "rb");
if (!rand_reader) {
perror("Failed to open /dev/urandom");
return 1;
}
// number of dices thrown
int number_of_dice = 0;
// array with the number of each type of dice
Dices types_and_number_of_dices[7] = {
{100000, D4}, {100000, D6}, {100000, D8}, {100000, D10},
{100000, D12}, {100000, D20}, {100000, D100}};
int index_dices = 0;
while (index_dices < 7) {
int number_of_generations = types_and_number_of_dices[index_dices].n;
printf("D%d: ", types_and_number_of_dices[index_dices].dices_type);
for (int i = 0; i < number_of_generations; i++) {
int value = simple_discard_method(
types_and_number_of_dices[index_dices].dices_type, rand_reader);
printf("%d ", value);
}
printf("\n");
index_dices++;
}
fclose(rand_reader);
return 0;
}
📝 Validation Engine & Source Code (`analyze.py`)
The statistical analyzer repeatedly executes the compiled binary, aggregates hundreds of thousands of rolls across all dice categories, runs Chi-Square goodness-of-fit tests, and exports structured results into CSV reports and Markdown documentation.
#!/usr/bin/env python3
"""
Dice Balance Analyzer
Calls ./main NUM_RUNS times, captures its stdout each time,
accumulates all dice rolls, computes balance statistics,
and exports a final CSV summary.
Usage: python analyze.py
python analyze.py --runs 200
python analyze.py --csv output.csv
"""
import sys
import os
import csv
import subprocess
from collections import defaultdict, Counter
from datetime import datetime
DEFAULT_RUNS = 100
WARN_THRESHOLD = 2.0
BAR_WIDTH = 20
DEFAULT_CSV = "dice_results.csv"
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
MAIN_EXE = os.path.join(SCRIPT_DIR, "main")
MAX_FACES = {"D4": 4, "D6": 6, "D8": 8, "D10": 10, "D12": 12, "D20": 20, "D100": 100}
def get_arg(flag: str, default: str | None = None) -> str | None:
args = sys.argv[1:]
if flag in args:
idx = args.index(flag)
if idx + 1 < len(args):
return args[idx + 1]
return default
def get_runs() -> int:
val = get_arg("--runs")
try:
return int(val) if val else DEFAULT_RUNS
except ValueError:
return DEFAULT_RUNS
def get_csv_path() -> str:
return get_arg("--csv") or DEFAULT_CSV
def parse_output(raw: str) -> dict[str, list[int]]:
result: dict[str, list[int]] = {}
for line in raw.strip().splitlines():
line = line.strip()
if not line or ":" not in line:
continue
label, _, values = line.partition(":")
label = label.strip().upper()
try:
nums = [int(x) for x in values.split() if x.strip()]
if nums:
result[label] = nums
except ValueError:
pass
return result
def call_main(run_index: int) -> dict[str, list[int]] | None:
try:
proc = subprocess.run(
[MAIN_EXE],
capture_output=True,
text=True,
timeout=30,
)
if proc.returncode != 0:
return None
parsed = parse_output(proc.stdout)
if not parsed:
return None
return parsed
except Exception:
return None
def collect_all_runs(num_runs: int) -> tuple[dict[str, Counter], int]:
accumulated: dict[str, Counter] = defaultdict(Counter)
success = 0
for i in range(1, num_runs + 1):
run_data = call_main(i)
if run_data is None:
continue
for die, rolls in run_data.items():
accumulated[die].update(rolls)
success += 1
if success == 0:
sys.exit(1)
return accumulated, success
def compute_stats(counts: Counter, max_face: int) -> dict[int, float]:
total = sum(counts.values())
return {
face: (counts.get(face, 0) / total * 100.0) if total else 0.0
for face in range(1, max_face + 1)
}
def chi_square(pct: dict[int, float], label: str) -> tuple[float, str]:
n = len(pct)
expected = 100.0 / n
chi2 = sum((p - expected) ** 2 / expected for p in pct.values())
critical = {3: 7.81, 5: 11.07, 7: 14.07, 9: 16.92, 11: 19.68, 19: 30.14, 99: 123.22}
df = n - 1
crit = critical.get(df, 3.841 * df)
verdict = "BALANCED" if chi2 <= crit else "UNBALANCED"
return chi2, verdict
def build_csv_rows(
label: str,
max_face: int,
pct: dict[int, float],
counts: Counter,
chi2: float,
verdict: str,
ts: str,
) -> list[dict]:
expected = 100.0 / max_face
rows = []
for face in range(1, max_face + 1):
p = pct.get(face, 0.0)
delta = p - expected
rows.append(
{
"timestamp": ts,
"die": label,
"total_faces": max_face,
"face": face,
"total_rolls": counts.get(face, 0),
"simulated_pct": round(p, 4),
"expected_pct": round(expected, 4),
"delta_pct": round(delta, 4),
"chi2": round(chi2, 4),
"balance": verdict,
"anomaly_flag": "YES" if abs(delta) >= WARN_THRESHOLD else "NO",
}
)
return rows
CANONICAL_ORDER = ["D4", "D6", "D8", "D10", "D12", "D20", "D100"]
def main() -> None:
num_runs = get_runs()
csv_path = get_csv_path()
accumulated, successful_runs = collect_all_runs(num_runs)
keys = [k for k in CANONICAL_ORDER if k in accumulated]
keys += [k for k in accumulated if k not in CANONICAL_ORDER]
ts: str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
all_csv_rows: list[dict] = []
for label in keys:
counts = accumulated[label]
max_face = MAX_FACES.get(label, max(counts.keys()))
pct = compute_stats(counts, max_face)
chi2, verdict = chi_square(pct, label)
all_csv_rows.extend(
build_csv_rows(label, max_face, pct, counts, chi2, verdict, ts)
)
file_exists = os.path.isfile(csv_path)
with open(csv_path, "a" if file_exists else "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=[
"timestamp", "die", "total_faces", "face", "total_rolls",
"simulated_pct", "expected_pct", "delta_pct", "chi2", "balance", "anomaly_flag"
])
if not file_exists:
writer.writeheader()
writer.writerows(all_csv_rows)
if __name__ == "__main__":
main()
📥 Download Source & Scripts
Download the required files to run local entropy and statistical validation:
How to run the validation:
- Download and compile the C source file via terminal:
gcc main.c -o main -O3
- Ensure the compiled
main executable is placed in the same directory as the Python scripts.
- Run the Python analyzer to gather data and generate the CSV report:
python analyze.py --runs 50
- Generate the Markdown summary report with:
python summary.py