nycki.net/static/mathdice/mathdice.js
nycki dab1ed15c1
All checks were successful
/ build (push) Successful in 45s
mathdice
2025-10-16 11:00:28 -07:00

143 lines
3.6 KiB
JavaScript

"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)}`;
}