UNPKG

dice-table

Version:

CLI and SDK for tabletop RPG dice rolling

366 lines (324 loc) 9.89 kB
/** * Dice Table - Main Library API * Clean, intuitive API for dice rolling and tabletop gaming utilities */ const { parse } = require('./lib/parser'); const { validate } = require('./lib/validator'); const { rollDice, keepHighest, keepLowest, dropHighest, dropLowest, sumKept, applyFloor } = require('./lib/roller'); const { formatCli, formatData, createDetailedBreakdown } = require('./lib/formatter'); const { analyze } = require('./lib/stats'); /** * Execute a single dice roll based on an expression * @param {string} expression - Dice expression like "2d6+5", "1d20", "4d6kh3" * @returns {Object} Roll result with full details */ function roll(expression) { // Validate expression first const validation = validate(expression); if (!validation.valid) { return { error: validation.error, expression, valid: false }; } // Parse the expression const parsed = parse(expression); if (parsed.error || !parsed.valid) { return { error: parsed.error || 'Failed to parse expression', expression, valid: false }; } try { const dice = []; let diceTotal = 0; let totalModifier = 0; let globalTotalFloor = null; // Process each part in the expression for (const part of parsed.parts) { if (part.modifier !== undefined) { // This is a modifier totalModifier += part.modifier; } else if (part.count && part.sides) { // This is a dice group const { count, sides, floor, keep, drop, negative, totalFloor } = part; // Roll the dice let rolls = rollDice(count, sides); // Apply per-die floor if specified if (floor !== undefined) { rolls = applyFloor(rolls, floor); } // Apply keep/drop rules if (keep) { if (keep.type === 'highest') { rolls = keepHighest(rolls, keep.count); } else if (keep.type === 'lowest') { rolls = keepLowest(rolls, keep.count); } } else if (drop) { if (drop.type === 'highest') { rolls = dropHighest(rolls, drop.count); } else if (drop.type === 'lowest') { rolls = dropLowest(rolls, drop.count); } } // Add to results for (const roll of rolls) { dice.push({ type: `d${sides}`, value: roll.value, kept: roll.kept, dropped: !roll.kept }); } // Add to dice total (apply negative if needed) let partTotal = sumKept(rolls); if (negative) { partTotal = -partTotal; } diceTotal += partTotal; // Track total floor (use the highest if multiple specified) if (totalFloor !== undefined) { globalTotalFloor = globalTotalFloor === null ? totalFloor : Math.max(globalTotalFloor, totalFloor); } } } // Apply total floor if specified if (globalTotalFloor !== null) { diceTotal = Math.max(diceTotal, globalTotalFloor); } // Calculate final total (apply modifiers after total floor) const total = diceTotal + totalModifier; // Create detailed breakdown const details = createDetailedBreakdown(dice, totalModifier, total); return { expression: parsed.expression, dice, modifier: totalModifier, total, details, valid: true }; } catch (error) { return { error: `Roll error: ${error.message}`, expression, valid: false }; } } /** * Parse a dice expression without rolling * @param {string} expression - Dice expression to parse * @returns {Object} Parsed structure or error */ function parseExpression(expression) { const validation = validate(expression); if (!validation.valid) { return { error: validation.error, expression, valid: false }; } const parsed = parse(expression); if (parsed.error || !parsed.valid) { return { error: parsed.error || 'Failed to parse expression', expression, valid: false }; } return { ...parsed, valid: true }; } /** * Validate a dice expression * @param {string} expression - Dice expression to validate * @returns {Object} Validation result */ function validateExpression(expression) { return validate(expression); } /** * Roll multiple times and return array of results * @param {string} expression - Dice expression to roll * @param {number} times - Number of times to roll (default: 1) * @returns {Array} Array of roll results */ function rollMany(expression, times = 1) { if (!Number.isInteger(times) || times < 1) { return [{ error: 'Times must be a positive integer', expression, valid: false }]; } if (times > 1000) { return [{ error: 'Cannot roll more than 1000 times in a single call', expression, valid: false }]; } const results = []; for (let i = 0; i < times; i++) { results.push(roll(expression)); } return results; } /** * Get statistical analysis of a dice expression * @param {string} expression - Dice expression to analyze * @param {Object} options - Analysis options * @param {number} options.iterations - Number of iterations for Monte Carlo simulation (default: 10000) * @returns {Object} Statistical analysis results */ function stats(expression, options = {}) { return analyze(expression, options); } /** * Roll dice and format for CLI display * @param {string} expression - Dice expression to roll * @param {Object} options - Formatting options * @returns {string} Formatted CLI output */ function rollCli(expression, options = {}) { const result = roll(expression); return formatCli(result, options); } /** * Roll dice and return structured data * @param {string} expression - Dice expression to roll * @returns {Object} Structured data suitable for APIs */ function rollData(expression) { const result = roll(expression); return formatData(result); } /** * Roll dice with enhanced detailed breakdown * @param {string} expression - Dice expression to roll * @returns {Object} Enhanced result with detailed breakdown */ function rollDetailed(expression) { // Validate expression first const validation = validate(expression); if (!validation.valid) { return { success: false, error: validation.error, expression }; } // Parse the expression const parsed = parse(expression); if (parsed.error || !parsed.valid) { return { success: false, error: parsed.error || 'Failed to parse expression', expression }; } try { const dice = []; const modifiers = []; let diceTotal = 0; let globalTotalFloor = null; // Process each part in the expression for (const part of parsed.parts) { if (part.modifier !== undefined) { // This is a modifier modifiers.push({ value: Math.abs(part.modifier), operation: part.modifier >= 0 ? '+' : '-' }); } else if (part.count && part.sides) { // This is a dice group const { count, sides, floor, keep, drop, negative, totalFloor } = part; // Roll the dice let rolls = rollDice(count, sides); // Apply per-die floor if specified if (floor !== undefined) { rolls = applyFloor(rolls, floor); } // Apply keep/drop rules if (keep) { if (keep.type === 'highest') { rolls = keepHighest(rolls, keep.count); } else if (keep.type === 'lowest') { rolls = keepLowest(rolls, keep.count); } } else if (drop) { if (drop.type === 'highest') { rolls = dropHighest(rolls, drop.count); } else if (drop.type === 'lowest') { rolls = dropLowest(rolls, drop.count); } } // Add to results for (const roll of rolls) { dice.push({ type: `d${sides}`, value: roll.value, kept: roll.kept, dropped: !roll.kept }); } // Add to dice total (apply negative if needed) let partTotal = sumKept(rolls); if (negative) { partTotal = -partTotal; } diceTotal += partTotal; // Track total floor (use the highest if multiple specified) if (totalFloor !== undefined) { globalTotalFloor = globalTotalFloor === null ? totalFloor : Math.max(globalTotalFloor, totalFloor); } } } // Apply total floor if specified if (globalTotalFloor !== null) { diceTotal = Math.max(diceTotal, globalTotalFloor); } // Calculate totals (apply modifiers after total floor) const modifierTotal = modifiers.reduce((sum, mod) => { return sum + (mod.operation === '+' ? mod.value : -mod.value); }, 0); const finalTotal = diceTotal + modifierTotal; return { success: true, expression: parsed.expression, result: { dice, modifiers, breakdown: { diceTotal, modifierTotal, finalTotal } } }; } catch (error) { return { success: false, error: `Roll error: ${error.message}`, expression }; } } // Export main API functions module.exports = { roll, rollDetailed, parse: parseExpression, validate: validateExpression, rollMany, stats, rollCli, rollData };