diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1a6bd45 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +package-lock.json binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e7ea1e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,117 @@ +## Node +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.pnp.* diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..b8c8a77 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,123 @@ +const path = require("path"); + +module.exports = grunt => { + grunt.initConfig({ + pkg: grunt.file.readJSON("package.json"), + clean: { + default: ["dist/"], + }, + copy: { + html: { + files: [{expand: true, cwd: "src/main/", src: "**/*.html", dest: "dist/"}] + }, + css: { + files: [{expand: true, cwd: "src/main/", src: "**/*.css", dest: "dist/"}] + }, + }, + focus: { + dev: { + include: ["css", "html", "js", "link"], + }, + }, + replace: { + dev: { + src: ["./dist/*.html", "./dist/*.js"], + replacements: [ + { + from: "%%VERSION_NUMBER%%", + to: "<%= pkg.version %>+" + new Date().toISOString().slice(0, 19).replace(/[-:T]/g, "") + } + ], + overwrite: true + }, + deploy: { + src: ["./dist/*.html", "./dist/*.js"], + replacements: [ + { + from: "%%VERSION_NUMBER%%", + to: "<%= pkg.version %>" + } + ], + overwrite: true + }, + }, + webpack: { + options: { + entry: "./src/main/js/index.js", + module: { + rules: [ + { + test: /\.js$/, + exclude: /node_modules/, + }, + ], + }, + resolve: { + extensions: [".js"], + }, + output: { + filename: "bundle.js", + path: path.resolve(__dirname, "dist/"), + }, + }, + dev: { + mode: "development", + devtool: "inline-source-map", + }, + deploy: { + mode: "production", + }, + }, + watch: { + css: { + files: ["src/main/**/*.css"], + tasks: ["copy:css"], + }, + html: { + files: ["src/main/**/*.html"], + tasks: ["copy:html"], + }, + js: { + files: ["src/main/**/*.js"], + tasks: ["webpack:dev", "replace:dev"], + }, + link: { + files: ["node_modules/@fwdekker/*/dist/**"], + tasks: ["webpack:dev", "replace:dev"], + }, + }, + }); + + grunt.loadNpmTasks("grunt-contrib-clean"); + grunt.loadNpmTasks("grunt-contrib-copy"); + grunt.loadNpmTasks("grunt-contrib-watch"); + grunt.loadNpmTasks("grunt-focus"); + grunt.loadNpmTasks("grunt-text-replace"); + grunt.loadNpmTasks("grunt-webpack"); + + grunt.registerTask("dev", [ + // Pre + "clean", + // Copy files + "copy:html", + "copy:css", + // Compile + "webpack:dev", + // Post + "replace:dev" + ]); + grunt.registerTask("dev:server", ["dev", "focus:dev"]); + grunt.registerTask("deploy", [ + // Pre + "clean", + // Copy files + "copy:html", + "copy:css", + // Compile JS + "webpack:deploy", + // Post + "replace:deploy" + ]); + + grunt.registerTask("default", ["dev"]); +}; diff --git a/README.md b/README.md index c1e62c7..1d5a49f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,30 @@ # Dice Given a set of dice, calculates the probability density graph of each value that can be rolled. + +## Development +### Requirements +* [npm](https://www.npmjs.com/) + +### Setting up +```shell script +# Install dependencies (only needed once) +$> npm ci +``` + +### Building +```shell script +# Build the template in `dist/` for development +$> npm run dev +# Same as above, but automatically rerun it whenever files are changed +$> npm run dev:server +# Build the template in `dist/` for deployment +$> npm run deploy +``` + +### Publishing +```shell script +# Log in to npm +$> npm login +# Push to npm +$> npm publish --access public +``` diff --git a/index.html b/index.html deleted file mode 100644 index ebb3f01..0000000 --- a/index.html +++ /dev/null @@ -1,310 +0,0 @@ - - - - - - - - - - - Dice probabilities | FWDekker - - - - - -
- -
-
-

Dice probabilities

- -
-

Calculates the probability of rolling a value given a combination of dice.

-
-
-
- - - -
-
-
-
-
- - - - - - - - - - - - - -
Sides per dieNumber of rolls
- -
- - -
-
-
-
-
- - - -
-

Probabilities

-
-
- -
-
-
- - - - -
- - - - - - - - diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..889b318 Binary files /dev/null and b/package-lock.json differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..aeb946e --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "dice", + "version": "1.0.9", + "description": "Calculates the probability of rolling a value given a combination of dice.", + "author": "Felix W. Dekker", + "browser": "dist/bundle.js", + "repository": { + "type": "git", + "url": "git@git.fwdekker.com:FWDekker/dice.git" + }, + "private": true, + "scripts": { + "clean": "grunt clean", + "dev": "grunt dev", + "dev:server": "grunt dev:server", + "deploy": "grunt deploy" + }, + "dependencies": { + "@fwdekker/template": "^0.0.13", + "chart.js": "^2.9.3" + }, + "devDependencies": { + "grunt": "^1.1.0", + "grunt-cli": "^1.3.2", + "grunt-contrib-clean": "^2.0.0", + "grunt-contrib-copy": "^1.0.0", + "grunt-contrib-watch": "^1.1.0", + "grunt-focus": "^1.0.0", + "grunt-text-replace": "^0.4.0", + "grunt-webpack": "^3.1.3", + "webpack": "^4.42.1", + "webpack-cli": "^3.3.11" + } +} diff --git a/src/main/index.html b/src/main/index.html new file mode 100644 index 0000000..e798406 --- /dev/null +++ b/src/main/index.html @@ -0,0 +1,78 @@ + + + + + + + + + + + Dice probabilities | FWDekker + + + + + +
+ +
+ + + + +
+
+
+
+
+ + + + + + + + + + + + + +
Sides per dieNumber of rolls
+ +
+ + +
+
+
+
+
+ + +
+

Probabilities

+
+
+ +
+
+
+
+ +
+ + + + + + diff --git a/src/main/js/index.js b/src/main/js/index.js new file mode 100644 index 0000000..7005b0f --- /dev/null +++ b/src/main/js/index.js @@ -0,0 +1,238 @@ +import {$, doAfterLoad, footer, header, nav} from "@fwdekker/template"; +import Chart from "chart.js" + + +////// +/// +/// Helper functions +/// +////// + +const repeat = (value, length) => { + const zeroArray = []; + for (let i = 0; i < length; i++) + zeroArray.push(value); + return zeroArray; +}; + +const rangeExclusive = (from, to) => { + const rangeArray = []; + for (let i = from; i < to; i++) + rangeArray.push(i); + return rangeArray; +}; + +const rangeInclusive = (from, to) => { + const rangeArray = rangeExclusive(from, to); + rangeArray.push(to); + return rangeArray; +}; + +const iterateNodeList = (nodeList, fun) => { + for (let i = 0; i < nodeList.length; i++) { + const node = nodeList.item(i); + fun(node); + } +}; + + +////// +/// +/// Template +/// +////// + +doAfterLoad(() => { + $("#nav").appendChild(nav()); + $("#header").appendChild(header({ + title: "Dice", + description: "Calculate the probability of rolling a value given a combination of dice" + })); + $("#footer").appendChild(footer({ + author: "Felix W. Dekker", + authorURL: "https://fwdekker.com/", + license: "MIT License", + licenseURL: "https://git.fwdekker.com/FWDekker/dice/src/branch/master/LICENSE", + vcs: "git", + vcsURL: "https://git.fwdekker.com/FWDekker/dice/", + version: "v%%VERSION_NUMBER%%" + })); + $("main").style.display = null; +}); + + +////// +/// +/// Input +/// +////// + +const inputTable = {}; + + +// Functions +inputTable.getTable = () => $("#dieSettings tbody"); + +inputTable.dieRowCount = () => inputTable.getTable().querySelectorAll(".dieEyes").length; + +inputTable.highestDieRowIndex = () => { + const table = inputTable.getTable(); + + let highestDieRowIndex = -1; + iterateNodeList(table.getElementsByTagName("tr"), (node) => { + if ("index" in node.dataset) + highestDieRowIndex = Math.max(highestDieRowIndex, +node.dataset.index); + }); + + return highestDieRowIndex; +}; + +inputTable.addDieRow = () => { + const createNumberInput = (index, className, value) => { + const input = document.createElement("input"); + input.id = className + index; + input.className = className; + input.type = "number"; + input.min = "1"; + input.step = "1"; + input.value = value; + input.addEventListener("keypress", (e) => { + if (e.key === "Enter") + outputChart.updateProbGraph(); + }); + input.focus(); + return input; + }; + + const createRemoveLink = (index, className) => { + const link = document.createElement("button"); + link.id = className + index; + link.className = className + " button-clear"; + link.type = "button"; + link.innerHTML = "Remove"; + link.onclick = (() => inputTable.removeDieRow(index)); + return link; + }; + + + const table = inputTable.getTable(); + const newIndex = inputTable.highestDieRowIndex() + 1; + + const row = table.insertRow(inputTable.dieRowCount()); + row.dataset.index = "" + newIndex; + + row.insertCell().appendChild(createNumberInput(newIndex, "dieEyes", 6)); + row.insertCell().appendChild(createNumberInput(newIndex, "dieCount", 2)); + row.insertCell().appendChild(createRemoveLink(newIndex, "dieRemove")); +}; + +inputTable.removeDieRow = index => { + if (inputTable.highestDieRowIndex() > 0) { + const table = inputTable.getTable(); + const row = table.querySelector("tr[data-index=\"" + index + "\"]"); + row.parentElement.removeChild(row); + } +}; + +inputTable.getDice = () => { + const dice = []; + + const eyesInputs = document.getElementsByClassName("dieEyes"); + const countInputs = document.getElementsByClassName("dieCount"); + for (let i = 0; i < eyesInputs.length; i++) { + const count = parseInt(countInputs.item(i).value); + + for (let j = 0; j < count; j++) + dice.push(parseInt(eyesInputs.item(i).value)); + } + + return dice; +}; + + +// Init +doAfterLoad(() => { + const button = $("#addDieRowButton"); + button.onclick = inputTable.addDieRow; + button.click() +}); + + +////// +/// +/// Output +/// +////// + +const outputChart = {}; +let probChart; + + +// Functions +outputChart.calculateDiceFrequencies = dice => { + if (dice.length === 0) + return []; + + + // Roll dice + let rollFreqs = [0].concat(repeat(1, dice[0])); + dice.slice(1).forEach(die => { + const dieRollFreqs = rollFreqs.concat(repeat(0, die)); + rollFreqs = repeat(0, dieRollFreqs.length); + + rangeInclusive(1, die).forEach(rollValue => { + rangeExclusive(1, dieRollFreqs.length - die).forEach(i => { + rollFreqs[rollValue + i] += dieRollFreqs[i]; + }); + }); + }); + rollFreqs.shift(); + + + // Calculate frequencies + const totalRolls = rollFreqs.reduce((a, b) => a + b, 0); + rangeExclusive(0, rollFreqs.length).forEach(roll => { + rollFreqs[roll] = rollFreqs[roll] / totalRolls; + }); + + + return rollFreqs; +}; + +outputChart.updateProbGraph = () => { + const dice = inputTable.getDice(); + const rollFreqs = outputChart.calculateDiceFrequencies(dice); + + probChart.data.labels = rangeInclusive(1, rollFreqs.length); + probChart.data.datasets = [{ + data: rollFreqs, + backgroundColor: "rgb(0, 51, 204, 0.4)" + }]; + probChart.update(); +}; + + +// Init +doAfterLoad(() => { + probChart = new Chart($("#probChart").getContext("2d"), { + type: "line", + data: {}, + options: { + legend: { + display: false + }, + scales: { + yAxes: [{ + display: true, + ticks: { + suggestedMin: 0 + } + }] + } + } + }); + + const submit = $("#submit"); + submit.onclick = outputChart.updateProbGraph; + submit.click() +});