Skip to content

Instantly share code, notes, and snippets.

@brettcvz
Created July 3, 2017 20:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save brettcvz/dad5b7e5b9230c1e5031c2f1a96e435d to your computer and use it in GitHub Desktop.
Save brettcvz/dad5b7e5b9230c1e5031c2f1a96e435d to your computer and use it in GitHub Desktop.
Probability grapher/calculator for DnD 5e dice rolls
<!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 &amp; 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">&nbsp;</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">&nbsp;</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">&nbsp;</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">&nbsp;</span>
<label for="additional_add">Yes, add to result:</label>
<input type="radio" name="additional" value="additional_add" id="additional_add"/>
<span class="spacer">&nbsp;</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