Last active
June 29, 2024 13:06
-
-
Save mindbound/88e0c8e6225f20da1d508e387bfcb36d to your computer and use it in GitHub Desktop.
Converter from arbitrary dice rolls to 1d100
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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>χ<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