The Codex / Projects / Dice Roller / JavaScript
Tier 0 — Absolute Beginner

Dice Roller

Roll any combination of dice in NdM notation — Array.from, reduce, regex parsing.

🐍 Python 🟨 JavaScript

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

dice.mjs
// 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

dice.test.mjs
// 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

  1. Add advantage/disadvantage (roll twice, take higher/lower): 2d20kh1
  2. Add a modifier: 2d6+3
  3. Histogram mode: roll 1000 times and print a bar chart of the distribution
  4. 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

Try It Yourself