1. The Problem
Build a dice roller that accepts standard RPG notation like 2d6 (roll 2 six-sided dice) or 4d4 (roll 4 four-sided dice), shows each individual roll, and sums the total.
Why this project? Array generation with Array.from,
reduce for summing, parsing structured input strings, and a clear separation between
the rolling logic and the CLI presentation layer.
2. How to Think About This
The nDm notation has a pattern: a number, the letter "d", another number.
Parse with a regex or split on "d". Then roll n dice each with m sides.
Array.from({ length: n }, () => rollDie(m)) creates an array of n
random numbers in one expression — no loop needed.
3. The Build
// Roll one die with the given number of sides
function rollDie(sides) {
if (!Number.isInteger(sides) || sides < 2) {
throw new RangeError(`Die must have at least 2 sides (got ${sides})`);
}
return Math.floor(Math.random() * sides) + 1;
}
// Roll n dice each with m sides, return individual results and total
function rollDice(count, sides) {
if (!Number.isInteger(count) || count < 1) {
throw new RangeError(`Must roll at least 1 die (got ${count})`);
}
const rolls = Array.from({ length: count }, () => rollDie(sides));
const total = rolls.reduce((sum, n) => sum + n, 0);
return { rolls, total, count, sides, min: count, max: count * sides };
}
// Parse "2d6" → { count: 2, sides: 6 }
function parseNotation(notation) {
const match = notation.trim().toLowerCase().match(/^(\d+)d(\d+)$/);
if (!match) throw new Error(`Invalid notation "${notation}". Use NdM format, e.g. 2d6`);
return { count: parseInt(match[1], 10), sides: parseInt(match[2], 10) };
}
// CLI: node dice.mjs 2d6 or node dice.mjs 4d4 3d8
const notations = process.argv.slice(2);
if (notations.length === 0) {
console.log("Usage: node dice.mjs <notation> [<notation> ...]");
console.log("Example: node dice.mjs 2d6 1d20");
process.exit(0);
}
for (const notation of notations) {
try {
const { count, sides } = parseNotation(notation);
const { rolls, total, min, max } = rollDice(count, sides);
console.log(`
${notation.toUpperCase()}:`);
console.log(` Rolls: [${rolls.join(", ")}]`);
console.log(` Total: ${total} (range ${min}–${max})`);
if (count === 1 && total === sides) console.log(" Maximum roll!");
if (count === 1 && total === 1) console.log(" Minimum roll!");
} catch (e) {
console.error(` Error (${notation}):`, e.message);
}
}
4. Test & Prove
// Test the pure functions only (no CLI, no I/O)
function rollDie(sides) { return Math.floor(Math.random() * sides) + 1; }
function rollDice(count, sides) {
const rolls = Array.from({ length: count }, () => rollDie(sides));
return { rolls, total: rolls.reduce((s,n)=>s+n,0), count, sides };
}
function parseNotation(n) {
const m = n.trim().toLowerCase().match(/^(\d+)d(\d+)$/);
if (!m) throw new Error("Invalid");
return { count: parseInt(m[1],10), sides: parseInt(m[2],10) };
}
let pass=0, fail=0;
function test(desc,fn){try{fn();console.log(" ✓",desc);pass++;}catch(e){console.log(" ✗",desc,"—",e.message);fail++;}}
// Statistical: roll 1000 times, every result should be in range 1–sides
test("1d6 always in 1–6", () => {
for(let i=0;i<1000;i++){const r=rollDie(6); if(r<1||r>6) throw new Error(r);}
});
test("4d4 total always in 4–16", () => {
for(let i=0;i<200;i++){const r=rollDice(4,4); if(r.total<4||r.total>16) throw new Error(r.total);}
});
test("rollDice count matches", () => { const r=rollDice(3,6); if(r.rolls.length!==3) throw new Error(r.rolls.length); });
test("parseNotation 2d6", () => { const p=parseNotation("2d6"); if(p.count!==2||p.sides!==6) throw new Error(JSON.stringify(p)); });
test("parseNotation case-insensitive", () => { const p=parseNotation("3D8"); if(p.count!==3||p.sides!==8) throw new Error(); });
test("parseNotation rejects 'abc'", () => { try{parseNotation("abc");throw new Error("should throw");}catch(e){if(e.message==="should throw")throw e;} });
console.log(\`
\${pass} passed, \${fail} failed\`);
5. The Interface
Input: one or more dice notations as CLI arguments (2d6, 1d20, 4d4, etc.)
Output: individual rolls and total per notation. Special messages for max/min single-die rolls.
$ node dice.mjs 2d6 1d20 2D6: Rolls: [4, 5] Total: 9 (range 2–12) 1D20: Rolls: [20] Total: 20 (range 1–20) Maximum roll!
6. Run It
node dice.mjs 2d6 # classic
node dice.mjs 1d20 # D&D attack roll
node dice.mjs 4d6 4d6 4d6 # roll ability scores
node dice.test.mjs # run tests🎯 Try this next
- Add advantage/disadvantage (roll twice, take higher/lower):
2d20kh1 - Add a modifier:
2d6+3 - Histogram mode: roll 1000 times and print a bar chart of the distribution
- Build a browser version with animated dice faces using emoji: ⚀⚁⚂⚃⚄⚅
What you practised
Array.from({ length: n }, fn) for generating arrays, reduce for summing,
regex parsing for structured input, process.argv, and statistical testing
(run 1000 times and assert range invariants).
Reference: Arrays · Numbers & Math · Regular Expressions