Skip to content

Instantly share code, notes, and snippets.

@mindbound
Last active June 29, 2024 13:06
Show Gist options
  • Save mindbound/88e0c8e6225f20da1d508e387bfcb36d to your computer and use it in GitHub Desktop.
Save mindbound/88e0c8e6225f20da1d508e387bfcb36d to your computer and use it in GitHub Desktop.
Converter from arbitrary dice rolls to 1d100
<!DOCTYPE html>
<html lang = "en">
<head>
<meta charset = "UTF-8">
<meta name = "viewport" content = "width=device-width, initial-scale=1.0">
<title>The Percentifier</title>
<style>
body {
font-family: sans-serif;
margin: 20px;
}
textarea {
width: 100%;
height: 100px;
resize: none;
}
button {
margin-top: 10px;
}
pre {
background: #f4f4f4;
padding: 10px;
}
#plot {
width: 100%;
height: 500px;
}
#similarity {
margin-top: 10px;
}
</style>
<script src = "https://cdn.plot.ly/plotly-latest.min.js"></script>
</head>
<body>
<h1>The Percentifier</h1>
<h4>Converts arbitrary dice roll tables to 1d100</h2>
<div>
<input id = "diceFormula" placeholder = "Enter dice formula">
<select id = "modifierMode">
<option value = "+">+</option>
<option value = "-">-</option>
</select>
<input type = "number" id = "modifierValue" value = "0" min = "0">
</div>
<textarea id = "inputTable" placeholder = "Enter roll values or ranges, one per line" oninput = "this.style.height = '';this.style.height = this.scrollHeight + 'px'"></textarea>
<button onclick = "convertTable()">Convert</button>
<pre id = "outputTable"></pre>
<div id = "similarity"></div>
<div id = "plot"></div>
<script>
function parseDiceFormula(formula) {
const [numDice, numSides] = formula.split("d").map(Number);
return {
numDice: numDice,
numSides: numSides
};
}
function parseRange(range) {
const [start, end] = range.replaceAll(/\p{Dash}/gu, "-").replaceAll(/\s/g, "").split("-").map(Number);
return {
start: start,
end: end || start
};
}
function validateDiceFormula(formula) {
if (!/^\d+d\d+$/.test(formula)) {
return false;
}
const [numDice, numSides] = formula.split("d").map(Number);
return numDice > 0 && numSides > 0;
}
function calculateProbabilities(dice, sides) {
const outcomes = {};
const totalOutcomes = Math.pow(sides, dice);
function rollDice(rolls, sum) {
if (rolls === 0) {
outcomes[sum] = (outcomes[sum] || 0) + 1;
} else {
for (let i = 1; i <= sides; i++) {
rollDice(rolls - 1, sum + i);
}
}
}
rollDice(dice, 0);
for (const key in outcomes) {
outcomes[key] = outcomes[key] / totalOutcomes * 100;
}
return outcomes;
}
function getProbabilities(diceFormula, modifierMode, modifierValue) {
if (!validateDiceFormula(diceFormula)) {
alert("Enter a valid formula (e.g., 1d6, 2d8)");
return null;
}
const { numDice, numSides } = parseDiceFormula(diceFormula);
const baseProbabilities = calculateProbabilities(numDice, numSides);
const modifier = modifierMode === "+" ? parseInt(modifierValue) : -parseInt(modifierValue);
const adjustedProbabilities = {};
for (const [roll, prob] of Object.entries(baseProbabilities)) {
const adjustedRoll = parseInt(roll) + modifier;
if (adjustedRoll > 0) {
adjustedProbabilities[adjustedRoll] = prob;
}
}
return adjustedProbabilities;
}
function convertTable() {
const input = document.getElementById("inputTable").value.trim();
const diceFormula = document.getElementById("diceFormula").value.trim();
const modifierMode = document.getElementById("modifierMode").value;
const modifierValue = document.getElementById("modifierValue").value;
const lines = input.split("\n").map(line => line.trim()).filter(line => line);
const ranges = lines.map(parseRange);
const probabilities = getProbabilities(diceFormula, modifierMode, modifierValue);
if (modifierValue < 0) {
alert("Modifier value must be a non-negative number");
return;
}
if (!probabilities) {
return;
}
const minRoll = Math.min(...Object.keys(probabilities).map(Number));
const maxRoll = Math.max(...Object.keys(probabilities).map(Number));
const coveredNumbers = new Set();
ranges.forEach(range => {
for (let i = range.start; i <= range.end; i++) {
coveredNumbers.add(i);
}
});
for (let i = minRoll; i <= maxRoll; i++) {
if (!coveredNumbers.has(i)) {
alert(`The entire ${minRoll}-${maxRoll} range must be covered`);
return;
}
}
const totalWeight = Object.values(probabilities).reduce((sum, weight) => sum + weight, 0);
let cumulativeWeight = 0;
let output = [];
let currentStart = 1;
let inputProbabilities = {};
let outputProbabilities = {};
for (let i = 0; i < ranges.length; i++) {
const range = ranges[i];
let rangeWeight = 0;
for (let j = range.start; j <= range.end; j++) {
if (probabilities[j] !== undefined) {
rangeWeight += probabilities[j];
} else {
alert(`Roll ${j} is out of range for ${diceFormula} formula`);
return;
}
}
inputProbabilities[`${range.start}-${range.end}`] = (rangeWeight / totalWeight) * 100;
cumulativeWeight += rangeWeight;
let rangeEnd = Math.round((cumulativeWeight / totalWeight) * 100);
if (rangeEnd < currentStart) {
rangeEnd = currentStart;
}
if (rangeEnd >= 100) {
rangeEnd = 100;
output.push(`${currentStart}-${rangeEnd}`);
outputProbabilities[`${currentStart}-${rangeEnd}`] = (rangeEnd - currentStart + 1);
break;
}
if (currentStart === rangeEnd) {
output.push(`${currentStart}`);
outputProbabilities[`${currentStart}`] = 1;
} else {
output.push(`${currentStart}-${rangeEnd}`);
outputProbabilities[`${currentStart}-${rangeEnd}`] = (rangeEnd - currentStart + 1);
}
currentStart = rangeEnd + 1;
}
document.getElementById("outputTable").textContent = output.join("\n");
plotDistributions(inputProbabilities, outputProbabilities, diceFormula, modifierMode, modifierValue);
calculateSimilarity(inputProbabilities, outputProbabilities);
}
function plotDistributions(inputProbabilities, outputProbabilities, diceFormula, modifierMode, modifierValue) {
const inputData = {
x: Object.keys(inputProbabilities),
y: Object.values(inputProbabilities),
type: "scatter",
mode: "lines+markers",
name: modifierValue === "0" ? `Input (${diceFormula})` : `Input (${diceFormula}${modifierMode}${modifierValue})`
};
const outputData = {
x: Object.keys(outputProbabilities),
y: Object.values(outputProbabilities),
type: "scatter",
mode: "lines+markers",
name: "Output (1d100)"
};
const layout = {
title: "Distribution Comparison",
xaxis: {
title: "Range",
type: "category"
},
yaxis: {
title: "Probability (%)"
}
};
Plotly.newPlot("plot", [inputData, outputData], layout, {displaylogo: false});
}
function calculateSimilarity(inputProbabilities, outputProbabilities) {
const keys = Object.keys(inputProbabilities);
let chiSquare = 0;
let klDivergence = 0;
for (let key of keys) {
const observed = (outputProbabilities[key] || 0);
const expected = inputProbabilities[key];
chiSquare += Math.pow(observed - expected, 2) / expected;
if (observed > 0) {
klDivergence += observed * Math.log(observed / expected);
}
}
document.getElementById("similarity").innerHTML = `<p>&chi;<sup>2</sup> similarity: ${chiSquare.toFixed(2)}</p><p>KL divergence: ${klDivergence.toFixed(2)}</p>`;
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment