dice-table
Version:
CLI and SDK for tabletop RPG dice rolling
366 lines (324 loc) • 9.89 kB
JavaScript
/**
* 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
};