roll_dice
Generate dice rolls for tabletop RPGs using standard notation like 2d6+4. Supports single rolls, batch processing, and advantage/disadvantage mechanics for d20 rolls.
Instructions
Roll dice using standard notation (e.g., "2d6+4", "4d6kh3"). Supports single rolls or batch rolling multiple expressions at once. Supports advantage/disadvantage for d20 rolls. Provide either "expression" for single roll or "batch" for multiple rolls.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| expression | No | Dice expression for single roll (e.g., "2d6+4", "1d20", "4d6kh3") | |
| batch | No | Array of roll requests for batch rolling (alternative to expression) | |
| reason | No | Optional reason for the roll(s) | |
| advantage | No | Roll with advantage (2d20, keep highest). Only works with single d20 rolls. | |
| disadvantage | No | Roll with disadvantage (2d20, keep lowest). Only works with single d20 rolls. |
Implementation Reference
- src/registry.ts:185-309 (handler)Main handler for roll_dice tool. Handles single and batch dice rolls, advantage/disadvantage logic for d20, validation, and delegates rolling to parseDice and formatting to formatDiceResult. Returns formatted markdown output.handler: async (args) => { // Check if this is a batch operation const argsObj = args as Record<string, unknown>; if ('batch' in argsObj && argsObj.batch) { const { batch, reason } = args as { batch?: Array<{ expression: string; label?: string; advantage?: boolean; disadvantage?: boolean }>; reason?: string; }; if (!batch || batch.length === 0) { return error('Missing required parameter: batch (must be non-empty array)'); } if (batch.length > 20) { return error('Too many rolls (maximum 20 per batch)'); } try { const results: Array<{ label?: string; expression: string; total: number; rolls: number[]; kept: number[] }> = []; for (const roll of batch) { if (!roll.expression) { return error('Each roll must have an expression'); } if (roll.advantage && roll.disadvantage) { return error(`Roll "${roll.label || roll.expression}": Cannot have both advantage and disadvantage`); } let finalExpression = roll.expression; // Auto-convert d20 rolls to advantage/disadvantage if ((roll.advantage || roll.disadvantage) && roll.expression.match(/^1d20([+-]\d+)?$/i)) { const modifier = roll.expression.match(/([+-]\d+)$/)?.[1] || ''; finalExpression = roll.advantage ? `2d20kh1${modifier}` : `2d20kl1${modifier}`; } else if (roll.advantage || roll.disadvantage) { return error(`Roll "${roll.label || roll.expression}": Advantage/disadvantage only works with single d20 rolls`); } const result = parseDice(finalExpression); results.push({ label: roll.label, expression: finalExpression, total: result.total, rolls: result.rolls, kept: result.kept, }); } // Format batch output const content: string[] = []; if (reason) { content.push(reason.toUpperCase()); content.push(''); } content.push(`ROLLING ${results.length} DICE ${results.length === 1 ? 'EXPRESSION' : 'EXPRESSIONS'}`); content.push(''); content.push('─'.repeat(40)); content.push(''); for (let i = 0; i < results.length; i++) { const r = results[i]; const label = r.label || `Roll ${i + 1}`; content.push(`${label}:`); content.push(` Expression: ${r.expression}`); if (r.rolls.length !== r.kept.length) { content.push(` Rolled: [${r.rolls.join(', ')}]`); content.push(` Kept: [${r.kept.join(', ')}]`); } else if (r.rolls.length > 1) { content.push(` Rolled: [${r.rolls.join(', ')}]`); } content.push(` Result: ${r.total}`); content.push(''); } content.push('─'.repeat(40)); content.push(''); content.push(`TOTAL ACROSS ALL ROLLS: ${results.reduce((sum, r) => sum + r.total, 0)}`); return success(createBox('BATCH ROLL', content, undefined, 'HEAVY')); } catch (err) { const message = err instanceof Error ? err.message : String(err); return error(message); } } // Single roll mode const { expression, reason, advantage, disadvantage } = args as { expression?: string; reason?: string; advantage?: boolean; disadvantage?: boolean; }; if (!expression) { return error('Missing required parameter: expression'); } if (advantage && disadvantage) { return error('Cannot have both advantage and disadvantage'); } try { let finalExpression = expression; // Auto-convert d20 rolls to advantage/disadvantage if ((advantage || disadvantage) && expression.match(/^1d20([+-]\d+)?$/i)) { const modifier = expression.match(/([+-]\d+)$/)?.[1] || ''; finalExpression = advantage ? `2d20kh1${modifier}` : `2d20kl1${modifier}`; } else if (advantage || disadvantage) { return error('Advantage/disadvantage only works with single d20 rolls (e.g., "1d20" or "1d20+5")'); } const result = parseDice(finalExpression); return success(formatDiceResult(result, reason)); } catch (err) { const message = err instanceof Error ? err.message : String(err); return error(message); } },
- src/registry.ts:137-183 (schema)Input schema definition for roll_dice tool, supporting single expression or batch array of rolls with optional labels, advantage/disadvantage, and reason.type: 'object', properties: { expression: { type: 'string', description: 'Dice expression for single roll (e.g., "2d6+4", "1d20", "4d6kh3")', }, batch: { type: 'array', description: 'Array of roll requests for batch rolling (alternative to expression)', items: { type: 'object', properties: { expression: { type: 'string', description: 'Dice expression (e.g., "2d6+4", "1d20", "4d6kh3")', }, label: { type: 'string', description: 'Label for this roll (e.g., "Attack 1", "Damage", "Goblin 1")', }, advantage: { type: 'boolean', description: 'Roll with advantage (2d20, keep highest)', }, disadvantage: { type: 'boolean', description: 'Roll with disadvantage (2d20, keep lowest)', }, }, required: ['expression'], }, minItems: 1, maxItems: 20, }, reason: { type: 'string', description: 'Optional reason for the roll(s)', }, advantage: { type: 'boolean', description: 'Roll with advantage (2d20, keep highest). Only works with single d20 rolls.', }, disadvantage: { type: 'boolean', description: 'Roll with disadvantage (2d20, keep lowest). Only works with single d20 rolls.', }, },
- src/registry.ts:133-310 (registration)Registration of the roll_dice tool in the static toolRegistry export.roll_dice: { name: 'roll_dice', description: 'Roll dice using standard notation (e.g., "2d6+4", "4d6kh3"). Supports single rolls or batch rolling multiple expressions at once. Supports advantage/disadvantage for d20 rolls. Provide either "expression" for single roll or "batch" for multiple rolls.', inputSchema: { type: 'object', properties: { expression: { type: 'string', description: 'Dice expression for single roll (e.g., "2d6+4", "1d20", "4d6kh3")', }, batch: { type: 'array', description: 'Array of roll requests for batch rolling (alternative to expression)', items: { type: 'object', properties: { expression: { type: 'string', description: 'Dice expression (e.g., "2d6+4", "1d20", "4d6kh3")', }, label: { type: 'string', description: 'Label for this roll (e.g., "Attack 1", "Damage", "Goblin 1")', }, advantage: { type: 'boolean', description: 'Roll with advantage (2d20, keep highest)', }, disadvantage: { type: 'boolean', description: 'Roll with disadvantage (2d20, keep lowest)', }, }, required: ['expression'], }, minItems: 1, maxItems: 20, }, reason: { type: 'string', description: 'Optional reason for the roll(s)', }, advantage: { type: 'boolean', description: 'Roll with advantage (2d20, keep highest). Only works with single d20 rolls.', }, disadvantage: { type: 'boolean', description: 'Roll with disadvantage (2d20, keep lowest). Only works with single d20 rolls.', }, }, }, handler: async (args) => { // Check if this is a batch operation const argsObj = args as Record<string, unknown>; if ('batch' in argsObj && argsObj.batch) { const { batch, reason } = args as { batch?: Array<{ expression: string; label?: string; advantage?: boolean; disadvantage?: boolean }>; reason?: string; }; if (!batch || batch.length === 0) { return error('Missing required parameter: batch (must be non-empty array)'); } if (batch.length > 20) { return error('Too many rolls (maximum 20 per batch)'); } try { const results: Array<{ label?: string; expression: string; total: number; rolls: number[]; kept: number[] }> = []; for (const roll of batch) { if (!roll.expression) { return error('Each roll must have an expression'); } if (roll.advantage && roll.disadvantage) { return error(`Roll "${roll.label || roll.expression}": Cannot have both advantage and disadvantage`); } let finalExpression = roll.expression; // Auto-convert d20 rolls to advantage/disadvantage if ((roll.advantage || roll.disadvantage) && roll.expression.match(/^1d20([+-]\d+)?$/i)) { const modifier = roll.expression.match(/([+-]\d+)$/)?.[1] || ''; finalExpression = roll.advantage ? `2d20kh1${modifier}` : `2d20kl1${modifier}`; } else if (roll.advantage || roll.disadvantage) { return error(`Roll "${roll.label || roll.expression}": Advantage/disadvantage only works with single d20 rolls`); } const result = parseDice(finalExpression); results.push({ label: roll.label, expression: finalExpression, total: result.total, rolls: result.rolls, kept: result.kept, }); } // Format batch output const content: string[] = []; if (reason) { content.push(reason.toUpperCase()); content.push(''); } content.push(`ROLLING ${results.length} DICE ${results.length === 1 ? 'EXPRESSION' : 'EXPRESSIONS'}`); content.push(''); content.push('─'.repeat(40)); content.push(''); for (let i = 0; i < results.length; i++) { const r = results[i]; const label = r.label || `Roll ${i + 1}`; content.push(`${label}:`); content.push(` Expression: ${r.expression}`); if (r.rolls.length !== r.kept.length) { content.push(` Rolled: [${r.rolls.join(', ')}]`); content.push(` Kept: [${r.kept.join(', ')}]`); } else if (r.rolls.length > 1) { content.push(` Rolled: [${r.rolls.join(', ')}]`); } content.push(` Result: ${r.total}`); content.push(''); } content.push('─'.repeat(40)); content.push(''); content.push(`TOTAL ACROSS ALL ROLLS: ${results.reduce((sum, r) => sum + r.total, 0)}`); return success(createBox('BATCH ROLL', content, undefined, 'HEAVY')); } catch (err) { const message = err instanceof Error ? err.message : String(err); return error(message); } } // Single roll mode const { expression, reason, advantage, disadvantage } = args as { expression?: string; reason?: string; advantage?: boolean; disadvantage?: boolean; }; if (!expression) { return error('Missing required parameter: expression'); } if (advantage && disadvantage) { return error('Cannot have both advantage and disadvantage'); } try { let finalExpression = expression; // Auto-convert d20 rolls to advantage/disadvantage if ((advantage || disadvantage) && expression.match(/^1d20([+-]\d+)?$/i)) { const modifier = expression.match(/([+-]\d+)$/)?.[1] || ''; finalExpression = advantage ? `2d20kh1${modifier}` : `2d20kl1${modifier}`; } else if (advantage || disadvantage) { return error('Advantage/disadvantage only works with single d20 rolls (e.g., "1d20" or "1d20+5")'); } const result = parseDice(finalExpression); return success(formatDiceResult(result, reason)); } catch (err) { const message = err instanceof Error ? err.message : String(err); return error(message); } }, },
- src/modules/dice.ts:20-87 (helper)Core helper function that parses dice expression (NdX modifiers), rolls random dice, applies keep/drop highest/lowest logic, adds modifier, and computes total.export function parseDice(expression: string): DiceResult { const cleaned = expression.replace(/\s/g, '').toLowerCase(); const match = cleaned.match(DICE_REGEX); if (!match) { throw new Error(`Invalid dice expression: "${expression}"`); } const numDice = parseInt(match[1]); const dieSize = parseInt(match[2]); const keepDrop = match[3] || ''; const modifierStr = match[4] || ''; if (numDice < 1 || numDice > 100) { throw new Error(`Invalid number of dice: ${numDice}`); } if (dieSize < 2 || dieSize > 1000) { throw new Error(`Invalid die size: ${dieSize}`); } // Roll dice const rolls: number[] = []; for (let i = 0; i < numDice; i++) { rolls.push(Math.floor(Math.random() * dieSize) + 1); } // Apply keep/drop modifier let kept = [...rolls]; if (keepDrop) { const type = keepDrop.slice(0, 2); const count = parseInt(keepDrop.slice(2)); if (count > rolls.length) { throw new Error(`Cannot keep/drop ${count} from ${rolls.length} dice`); } const sorted = [...rolls].sort((a, b) => b - a); // Descending switch (type) { case 'kh': // Keep highest kept = sorted.slice(0, count); break; case 'kl': // Keep lowest kept = sorted.slice(-count); break; case 'dh': // Drop highest kept = sorted.slice(count); break; case 'dl': // Drop lowest kept = sorted.slice(0, -count); break; } } // Parse modifier const modifier = modifierStr ? parseInt(modifierStr) : 0; // Calculate total const total = kept.reduce((sum, n) => sum + n, 0) + modifier; return { expression, rolls, kept, modifier, total, }; }
- src/modules/dice.ts:90-155 (helper)Helper function to format DiceResult into a beautiful markdown ASCII art box, with special displays for d20 crits, dice faces, calculations, and totals.export function formatDiceResult(result: DiceResult, reason?: string): string { const content: string[] = []; const box = BOX.LIGHT; // Title line content.push(`DICE ROLL: ${result.expression}`); if (reason) { content.push(`(${reason})`); } content.push(''); // For d20 critical hits/fails, show special display if (result.expression.toLowerCase().includes('d20') && result.rolls.length === 1) { const roll = result.rolls[0]; if (roll === 1 || roll === 20) { const special = D20_SPECIAL[roll]; special.forEach(line => content.push(line)); content.push(''); content.push(`TOTAL: ${result.total}`); return createBox('DICE ROLL', content, undefined, 'HEAVY'); } } // Show dice faces (for reasonable number of dice) if (result.rolls.length <= 6 && result.rolls.every(r => r <= 6)) { const diceFaces = renderDiceHorizontal(result.rolls); diceFaces.forEach(line => content.push(line)); content.push(''); // Show which were kept if different if (result.rolls.length !== result.kept.length) { const keptIndices = result.rolls.map((r, i) => result.kept.includes(r) && !result.kept.slice(0, result.kept.indexOf(r)).includes(r) ? i : -1 ).filter(i => i >= 0); content.push(`KEPT: [${result.kept.join(', ')}]`); content.push(`DROPPED: [${result.rolls.filter(r => !result.kept.includes(r)).join(', ')}]`); content.push(''); } } else { // For many dice or large dice, show numbers if (result.rolls.length !== result.kept.length) { content.push(`ROLLED: [${result.rolls.join(', ')}]`); content.push(`KEPT: [${result.kept.join(', ')}]`); } else { content.push(`ROLLED: [${result.rolls.join(', ')}]`); } content.push(''); } // Show calculation const diceSum = result.kept.reduce((sum, n) => sum + n, 0); if (result.modifier !== 0) { const sign = result.modifier > 0 ? '+' : ''; content.push(`CALCULATION: ${diceSum} ${sign}${result.modifier} = ${result.total}`); } else { content.push(`TOTAL: ${result.total}`); } // Add bottom divider content.push(''); content.push('─'.repeat(40)); // Will be adjusted by auto-sizing content.push(`FINAL RESULT: ${result.total}`); return createBox('DICE ROLL', content, undefined, 'HEAVY'); }