Created
July 3, 2017 20:24
-
-
Save brettcvz/dad5b7e5b9230c1e5031c2f1a96e435d to your computer and use it in GitHub Desktop.
Probability grapher/calculator for DnD 5e dice rolls
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> | |
<head> | |
<script | |
src="https://code.jquery.com/jquery-3.2.1.min.js" | |
integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" | |
crossorigin="anonymous"> | |
</script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/flot/0.8.3/jquery.flot.min.js"></script> | |
<style> | |
#results { | |
display: flex; | |
flex-direction: row; | |
} | |
#result-prob-curve { | |
width: 500px; | |
height: 300px; | |
} | |
#result-cdf-curve { | |
width: 500px; | |
height: 300px; | |
} | |
.spacer { | |
margin-right: 25px; | |
} | |
</style> | |
<script> | |
const RUN_COUNT = 50000; | |
function roll_die(sides) { | |
return Math.floor(Math.random() * sides) + 1; | |
} | |
function roll_dice(count, sides) { | |
const rolls = []; | |
for (var i = 0; i < count; i++) { | |
rolls[i] = roll_die(sides); | |
} | |
return rolls; | |
} | |
function simulate(configuration) { | |
// console.log(configuration); | |
// map of value: count | |
const results = {} | |
// Simulation | |
for (var i = 0; i < RUN_COUNT; i++) { | |
let rolls = roll_dice(configuration.die_count, configuration.die_sides); | |
if (configuration.reroll) { | |
// Reroll specified values | |
rolls = rolls.map((v) => ( | |
configuration.reroll.includes(v) ? roll_die(configuration.die_sides) : v | |
)) | |
} | |
if (configuration.subset.take !== "all") { | |
// Sorts ascending - annoyingly, javascript sorts by string value by defulat | |
rolls.sort((a, b) => a - b); | |
if (configuration.subset.take == "min") { | |
rolls = rolls.slice(0, configuration.subset.count); | |
} else if (configuration.subset.take == "max") { | |
rolls = rolls.slice(-1 * configuration.subset.count); | |
} | |
} | |
let sum = rolls.reduce((acc, cur) => acc + cur, 0); | |
if (configuration.additional) { | |
let additional_rolls = roll_dice(configuration.additional.die_count, | |
configuration.additional.die_sides); | |
let additional_sum = additional_rolls.reduce((acc, cur) => acc + cur, 0); | |
if (configuration.additional.modifier === "add") { | |
sum += additional_sum; | |
} else if (configuration.additional.modifier === "subtract") { | |
sum -= additional_sum; | |
} | |
} | |
const res = Math.round((configuration.factor || 1) * sum) + (configuration.offset || 0); | |
results[res] = (results[res] || 0) + 1; | |
} | |
return results; | |
} | |
function display(results) { | |
const probabilities = {}; | |
for (var group in results) { | |
probabilities[group] = {}; | |
let runningCount = 0; | |
const nums = Object.keys(results[group]); | |
nums.sort((a, b) => a - b); | |
nums.forEach((num) => { | |
const count = results[group][num]; | |
// "At least or greater" | |
const cumeProb = ((RUN_COUNT - runningCount) * 100.0 / RUN_COUNT).toFixed(1); | |
runningCount += count; | |
const probability = (count * 100.0 / RUN_COUNT).toFixed(1); | |
probabilities[group][num] = { | |
count, | |
runningCount, | |
probability, | |
cumeProb, | |
}; | |
}); | |
} | |
$("<div id='tooltip'></div>").css({ | |
position: "absolute", | |
display: "none", | |
border: "1px solid #fdd", | |
padding: "2px", | |
"background-color": "#fee", | |
opacity: 0.80 | |
}).appendTo("body"); | |
const showTooltip = (event, pos, item) => { | |
if (item) { | |
var x = item.datapoint[0].toFixed(2), | |
y = item.datapoint[1].toFixed(2), | |
label = item.series.label; | |
$("#tooltip").html(`${label} | ${x}: ${y}%`) | |
.css({top: item.pageY+5, left: item.pageX+5}) | |
.fadeIn(200); | |
} else { | |
$("#tooltip").hide(); | |
} | |
} | |
const probData = Object.keys(probabilities).map((label) => ({ | |
label, | |
data: Object.keys(probabilities[label]).map((n) => | |
[n, probabilities[label][n].probability] | |
), | |
})); | |
probData.forEach((d) => { | |
d.data.sort((a, b) => a[0] - b[0]); | |
}); | |
$.plot("#result-prob-curve", probData, { | |
series: { | |
lines: { | |
show: true, | |
}, | |
points: { | |
show: true, | |
}, | |
}, | |
grid: { | |
hoverable: true, | |
}, | |
}); | |
const cumeProbData = Object.keys(probabilities).map((label) => ({ | |
label, | |
data: Object.keys(probabilities[label]).map((n) => | |
[n, probabilities[label][n].cumeProb] | |
), | |
})); | |
cumeProbData.forEach((d) => { | |
d.data.sort((a, b) => a[0] - b[0]); | |
}); | |
$.plot("#result-cdf-curve", cumeProbData, { | |
series: { | |
lines: { | |
show: true, | |
}, | |
points: { | |
show: true, | |
}, | |
}, | |
yaxis: { | |
ticks: 5, | |
min: 0, | |
max: 100, | |
}, | |
grid: { | |
hoverable: true, | |
}, | |
}); | |
$("#result-prob-curve").bind("plothover", showTooltip); | |
$("#result-cdf-curve").bind("plothover", showTooltip); | |
} | |
$(function(){ | |
$("#form").submit(function(e){ | |
e.preventDefault(); | |
const configuration = { | |
die_count: parseInt($("#die_count").val(), 10), | |
die_sides: parseInt($("#die_type").val(), 10), | |
subset: { | |
take: $("input[type='radio'][name='subset']:checked").val(), | |
}, | |
offset: parseInt($("#offset").val(), 10), | |
factor: parseFloat($("#factor").val()), | |
}; | |
if (configuration.subset.take === "min") { | |
configuration.subset.count = parseInt($("#subset_n_min").val(), 10); | |
} | |
if (configuration.subset.take === "max") { | |
configuration.subset.count = parseInt($("#subset_n_max").val(), 10); | |
} | |
if ($("#reroll").is(":checked")) { | |
configuration.reroll = $("#reroll_die").val().map((d) => parseInt(d, 10)); | |
} | |
const additional = { | |
die_count: parseInt($("#additional_die_count").val(), 10), | |
die_sides: parseInt($("#additional_die_type").val(), 10), | |
}; | |
if ($("#additional_add").is(":checked")) { | |
configuration.additional = additional; | |
configuration.additional.modifier = "add"; | |
} | |
if ($("#additional_subtract").is(":checked")) { | |
configuration.additional = additional; | |
configuration.additional.modifier = "subtract"; | |
} | |
const results = simulate(configuration); | |
display({ | |
"custom": results | |
}); | |
return false; | |
}); | |
$("#advantage").click(function(e){ | |
e.preventDefault(); | |
display({ | |
"Normal d20": simulate({ die_count: 1, die_sides: 20, subset: { | |
take: "all"}}), | |
"Advantage": simulate({ die_count: 2, die_sides: 20, subset: { | |
take: "max", | |
count: 1 | |
}}), | |
"Disadvantage": simulate({ die_count: 2, die_sides: 20, subset: { | |
take: "min", | |
count: 1 | |
}}), | |
}); | |
return false; | |
}); | |
$("#gwf").click(function(e){ | |
e.preventDefault(); | |
display({ | |
"Normal 2d6": simulate({ die_count: 2, die_sides: 6, subset: { | |
take: "all"}}), | |
"Great Weapon Fighting": simulate({ die_count: 2, die_sides: 6, subset: { | |
take: "all" | |
}, | |
reroll: [1,2], | |
}), | |
}); | |
return false; | |
}); | |
$("#stats").click(function(e){ | |
e.preventDefault(); | |
display({ | |
"Stats": simulate({ die_count: 4, die_sides: 6, subset: { | |
take: "max", | |
count: 3, | |
}}), | |
}); | |
return false; | |
}); | |
$("#fireball").click(function(e){ | |
e.preventDefault(); | |
display({ | |
"Fireball": simulate({ die_count: 8, die_sides: 6, subset: { | |
take: "all"}}), | |
}); | |
return false; | |
}); | |
$("#guidance").click(function(e){ | |
e.preventDefault(); | |
display({ | |
"Normal d20": simulate({ die_count: 1, die_sides: 20, subset: { | |
take: "all"}}), | |
"With Guildance": simulate({ die_count: 1, die_sides: 20, subset: { | |
take: "all"}, | |
additional: { | |
die_count: 1, | |
die_sides: 4, | |
modifier: "add", | |
} | |
}), | |
}); | |
return false; | |
}); | |
$("#cuttingWords").click(function(e){ | |
e.preventDefault(); | |
display({ | |
"Normal d20": simulate({ die_count: 1, die_sides: 20, subset: { | |
take: "all"}}), | |
"With Cutting Words (d8)": simulate({ die_count: 1, die_sides: 20, subset: { | |
take: "all"}, | |
additional: { | |
die_count: 1, | |
die_sides: 8, | |
modifier: "subtract", | |
} | |
}), | |
}); | |
return false; | |
}); | |
}); | |
</script> | |
</head> | |
<body> | |
<h1>Calculate probabilities for various types of die rolls</h1> | |
<ul> | |
<li><a id="advantage" href="#">Advantage & Disadvantage on d20s</a></li> | |
<li><a id="gwf" href="#">Great weapon fighting w/ a 2d6</a></li> | |
<li><a id="stats" href="#">Rolling character stats (4d6, take top 3)</a></li> | |
<li><a id="fireball" href="#">Fireball</a></li> | |
<li><a id="guidance" href="#">Guidance</a></li> | |
<li><a id="cuttingWords" href="#">Cutting Words (d8)</a></li> | |
<li><a href="#custom">Custom configuration</a></li> | |
</ul> | |
<div id="output"> | |
<div id="results"> | |
<div id="probabilities"> | |
<h3>Probability of specific roll</h3> | |
<div id="result-prob-curve"></div> | |
</div> | |
<div id="cume-probabilities"> | |
<h3>Probability of at least</h3> | |
<div id="result-cdf-curve"></div> | |
</div> | |
</div> | |
</div> | |
<form id="form"> | |
<h2 id="custom">Your own custom configuration:</h2> | |
<fieldset> | |
<legend>Dice:</legend> | |
<label for="die_count">Number of Dice:</label> | |
<select id="die_count" name="die_count"> | |
<option value="1">1</option> | |
<option value="2">2</option> | |
<option value="3">3</option> | |
<option value="4">4</option> | |
<option value="5">5</option> | |
<option value="6">6</option> | |
<option value="7">7</option> | |
<option value="8">8</option> | |
<option value="9">9</option> | |
<option value="10">10</option> | |
</select> | |
<span class="spacer"> </span> | |
<label for="die_type">Type of Dice:</label> | |
<select id="die_type" name="die_type"> | |
<option value="4">d4</option> | |
<option value="6">d6</option> | |
<option value="8">d8</option> | |
<option value="10">d10</option> | |
<option value="12">d12</option> | |
<option value="20">d20</option> | |
<option value="100">d100</option> | |
</select> | |
</fieldset> | |
<fieldset> | |
<legend>Subsets:</legend> | |
<label for="subset_all">Take all</label> | |
<input id="subset_all" type="radio" name="subset" value="all" checked="checked"> | |
<span class="spacer"> </span> | |
<label for="subset_min">Take n lowest:</label> | |
<input id="subset_min" type="radio" name="subset" value="min"> | |
<select id="subset_n_min" name="subset_n_min"> | |
<option value="1">1</option> | |
<option value="2">2</option> | |
<option value="3">3</option> | |
<option value="4">4</option> | |
<option value="5">5</option> | |
<option value="6">6</option> | |
<option value="7">7</option> | |
<option value="8">8</option> | |
<option value="9">9</option> | |
<option value="10">10</option> | |
</select> | |
<span class="spacer"> </span> | |
<label for="subset_max">Take n highest:</label> | |
<input id="subset_max" type="radio" name="subset" value="max"> | |
<select id="subset_n_max" name="subset_n_max"> | |
<option value="1">1</option> | |
<option value="2">2</option> | |
<option value="3">3</option> | |
<option value="4">4</option> | |
<option value="5">5</option> | |
<option value="6">6</option> | |
<option value="7">7</option> | |
<option value="8">8</option> | |
<option value="9">9</option> | |
<option value="10">10</option> | |
</select> | |
</fieldset> | |
<fieldset> | |
<legend>Incorporate additional dice?</legend> | |
<p> | |
<label for="additional_no">No:</label> | |
<input type="radio" name="additional" value="additional_no" id="additional_no" checked/> | |
<span class="spacer"> </span> | |
<label for="additional_add">Yes, add to result:</label> | |
<input type="radio" name="additional" value="additional_add" id="additional_add"/> | |
<span class="spacer"> </span> | |
<label for="additional_subtract">Yes, subtract from result:</label> | |
<input type="radio" name="additional" value="additional_subtract" id="additional_subtract"/> | |
</p> | |
<p> | |
<label for="additional_die_count">Number of addtional dice:</label> | |
<select id="additional_die_count" name="additional_die_count"> | |
<option value="1">1</option> | |
<option value="2">2</option> | |
<option value="3">3</option> | |
<option value="4">4</option> | |
<option value="5">5</option> | |
<option value="6">6</option> | |
<option value="7">7</option> | |
<option value="8">8</option> | |
<option value="9">9</option> | |
<option value="10">10</option> | |
</select> | |
</p> | |
<p> | |
<label for="additional_die_type">Type of additional dice:</label> | |
<select id="additional_die_type" name="additional_die_type"> | |
<option value="4">d4</option> | |
<option value="6">d6</option> | |
<option value="8">d8</option> | |
<option value="10">d10</option> | |
<option value="12">d12</option> | |
<option value="20">d20</option> | |
<option value="100">d100</option> | |
</select> | |
</p> | |
</fieldset> | |
<fieldset> | |
<legend>Additional configuration:</legend> | |
<p> | |
<label for="offset">Add/subtract fixed amount:</label> | |
<input id="offset" type="number" value="0"/> | |
</p> | |
<p> | |
<label for="factor">Multiply by, with rounding of result:</label> | |
<input id="factor" type="number" value="1"/> | |
</p> | |
<p> | |
<label for="reroll">Re-roll certain values (once):</label> | |
<input id="reroll" type="checkbox" name="reroll" value="yes"/> | |
<label for="reroll_die">Select value(s) to reroll:</label> | |
<select id="reroll_die" multiple> | |
<option value="1">1's</option> | |
<option value="2">2's</option> | |
<option value="3">3's</option> | |
<option value="4">4's</option> | |
<option value="5">5's</option> | |
<option value="6">6's</option> | |
<option value="7">7's</option> | |
<option value="8">8's</option> | |
<option value="9">9's</option> | |
<option value="10">10's</option> | |
<option value="11">11's</option> | |
<option value="12">12's</option> | |
<option value="13">13's</option> | |
<option value="14">14's</option> | |
<option value="15">15's</option> | |
<option value="16">16's</option> | |
<option value="17">17's</option> | |
<option value="18">18's</option> | |
<option value="19">19's</option> | |
<option value="20">20's</option> | |
</select> | |
</p> | |
</fieldset> | |
<input type="submit" value="Simulate" /> | |
</form> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment