diff --git a/content/index.md b/content/index.md
index b9342c4..864d406 100644
--- a/content/index.md
+++ b/content/index.md
@@ -110,6 +110,12 @@ Sticker by [djuan](https://linktr.ee/mkiiisystem)!
**[Braceless JS](https://github.com/nycki93/braceless-javascript/), 2018.** You can do a lot with 'one-line' functions. Let's play with that!
+
+
+
+
+**[Mathdice](./mathdice/), 2017.** Practice your number skills with random puzzles. Check with the auto-solver!
+
diff --git a/static/a/card-mathdice.png b/static/a/card-mathdice.png
new file mode 100644
index 0000000..de371cc
Binary files /dev/null and b/static/a/card-mathdice.png differ
diff --git a/static/a/card-qrplay.png b/static/a/card-qrplay.png
index 75e0862..46d551f 100644
Binary files a/static/a/card-qrplay.png and b/static/a/card-qrplay.png differ
diff --git a/static/mathdice/index.html b/static/mathdice/index.html
new file mode 100644
index 0000000..f1fc5ad
--- /dev/null
+++ b/static/mathdice/index.html
@@ -0,0 +1,29 @@
+
+
+
+ Mathdice Solver 1.0
+
+
+
+
+
+
+
Your numbers are:
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/static/mathdice/mathdice.css b/static/mathdice/mathdice.css
new file mode 100644
index 0000000..e4c88e3
--- /dev/null
+++ b/static/mathdice/mathdice.css
@@ -0,0 +1,19 @@
+body {
+ font-family: sans-serif;
+}
+
+.dice {
+ display: inline-block;
+ box-sizing: border-box;
+ border: gray outset 6px;
+ text-align: center;
+
+ font-size: 80px;
+ width: 100px;
+ height: 100px;
+}
+
+#target {
+ border-color: gold;
+ width: 200px;
+}
diff --git a/static/mathdice/mathdice.js b/static/mathdice/mathdice.js
new file mode 100644
index 0000000..0b97fa1
--- /dev/null
+++ b/static/mathdice/mathdice.js
@@ -0,0 +1,143 @@
+"use strict";
+const OPERATORS = {
+ add: {
+ apply(a, b) {return a + b},
+ toString() {return "+"}
+ },
+ subtract: {
+ apply(a, b) {return a - b},
+ toString() {return "-"}
+ },
+ multiply: {
+ apply(a, b) {return a * b},
+ toString() {return "*"}
+ },
+ divide: {
+ apply(a, b) {return a / b},
+ toString() {return "/"}
+ },
+ power: {
+ apply(a, b) {return a ** b},
+ toString() {return "^"}
+ }
+}
+
+// Assign event handlers.
+document.getElementById("roll-button").onclick = generateProblem;
+document.getElementById("solve-button").onclick = showSolution;
+
+/**
+ * Roll dice and generate a mathdice problem. There are three "key" values
+ * (d6), and one "target" roll (1d12 * 1d12).
+ */
+function generateProblem () {
+ // mouse:mice :: douse:dice
+ const diceList = document.querySelectorAll('.scoring');
+ Array.prototype.forEach.call(diceList, douse => {
+ douse.value = roll_d(6);
+ })
+
+ const target = document.getElementById('target');
+ target.value = roll_d(12) * roll_d(12);
+}
+
+/**
+ * Solve the problem! Read in the values on screen and run them through the
+ * solution algorithm.
+ */
+function showSolution () {
+ const diceList = document.querySelectorAll('.scoring');
+ const diceValues = Array.prototype.map.call(diceList, douse =>
+ Number(douse.value)
+ )
+ const targetValue = Number(document.getElementById('target').value);
+ const solution = solve(diceValues, targetValue);
+ document.getElementById('solution').innerHTML = solution;
+}
+
+/**
+ * Generate a random number between 1 and n, inclusive.
+ * @param {number} n
+ */
+function roll_d (n) {
+ const roll = Math.floor(n * Math.random()) + 1;
+ return roll;
+}
+
+/**
+ * Evaluate an expression! An expression is either
+ * 1) a number, or
+ * 2) two expressions connected with an operator.
+ * @param {number | Array} expr
+ * @returns {number}
+ */
+ function evaluateExpression (expr) {
+ if (!Array.isArray(expr)) return expr;
+
+ const [a, op, b] = expr;
+ const valueA = evaluateExpression(a);
+ const valueB = evaluateExpression(b);
+ return op.apply(valueA, valueB);
+ }
+
+/**
+ * Write an expression as a string.
+ * @param {number | Array} expr
+ * @returns {string}
+ */
+function writeExpression (expr) {
+ if (!Array.isArray(expr)) return expr;
+
+ const [a, op, b] = expr;
+ let strA = writeExpression(a);
+ if (Array.isArray(a)) {strA = `(${strA})`}
+ let strB = writeExpression(b);
+ if (Array.isArray(b)) {strB = `(${strB})`}
+ return `${strA} ${op.toString()} ${strB}`;
+}
+
+/**
+ * Solve a mathdice problem!
+ * @param {number[]} scoringDice An array of "key" values used in the puzzle.
+ * @param {number} target The "target" value to try and produce.
+ * @returns {string} A string representation of the optimal answer.
+ */
+function solve(scoringDice, target) {
+ // Possible solutions: 2 * 3! * 5^2 = 300. Reasonable to brute force.
+ const [a, b, c] = scoringDice;
+ const dicePermutations = [
+ [a, b, c],
+ [a, c, b],
+ [b, a, c],
+ [b, c, a],
+ [c, a, b],
+ [c, b, a]
+ ];
+ const templates = [
+ (a, b, c, x, y) => [a, x, [b, y, c]],
+ (a, b, c, x, y) => [[a, x, b], y, c]
+ ];
+
+ // I don't even care how brutally hard-coded this is, I don't NEED the
+ // general case right now.
+ const guesses = [];
+ for (const keyX in OPERATORS) {
+ const x = OPERATORS[keyX];
+ for (const keyY in OPERATORS) {
+ const y = OPERATORS[keyY];
+ dicePermutations.forEach( ([a, b, c]) => {
+ templates.forEach( f => {
+ guesses.push(f(a, b, c, x, y));
+ })
+ })
+ }
+ }
+
+ // Find the best guess.
+ const scores = guesses.map( guess =>
+ Math.abs(target - evaluateExpression(guess))
+ )
+ const bestScore = Math.min(...scores);
+ const bestGuess = guesses[scores.indexOf(bestScore)];
+ return `${writeExpression(bestGuess)} = ${evaluateExpression(bestGuess)}`;
+}
diff --git a/static/mathdice/readme.md b/static/mathdice/readme.md
new file mode 100644
index 0000000..3ed07a9
--- /dev/null
+++ b/static/mathdice/readme.md
@@ -0,0 +1 @@
+An html mathdice game.