node-dice-js
Version:
Dice module that will parse, format, and execute dice notation commands
250 lines (201 loc) • 7.01 kB
JavaScript
var _ = require('lodash');
function Dice(options) {
var self = this;
var defaults = {
command: 'd20',
throttles: {
times: 100,
repeat: 100,
faces: 100,
multiplier: 100,
modifier: 100
}
};
self.options = _.assign(defaults, options);
self.data = {
command: null,
parsed: null,
outcomes: [],
text: [],
verbose: []
};
};
// validates that any value provided is less than our throttle value
Dice.prototype.throttle = function throttle() {
var self = this;
var parsed = self.data.parsed;
var throttles = self.options.throttles;
_.forOwn(parsed, function(value, key) {
if (value && typeof value === 'number' && _.has(throttles, key) && value > throttles[key]) {
throw new Error(key + ' (' + value + ') exceeds the limit of ' + throttles[key] + ' that has been imposed');
}
});
}
// rolls the die and returns the outcome
Dice.prototype.roll = function roll(faces) {
if (typeof faces !== 'number') {
throw new Error('`faces` must be a number');
}
var min = 1;
var max = faces;
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// execute command
Dice.prototype.execute = function execute(command) {
var self = this;
var data = self.data;
if (typeof command === 'undefined' || (typeof command === 'string' && !command.trim().length)) {
command = self.options.command;
}
var parsed = self.parse(command);
data.parsed = parsed;
data.command = command;
// throttle values provided
self.throttle();
_.times(data.parsed.repeat, function(n) {
var text = [];
var verbose = [];
var outcome = {
rolls: [],
total: 0
};
// make the rolls
_.times(data.parsed.times, function(n) {
var rolled = self.roll(data.parsed.faces);
outcome.rolls.push(rolled);
verbose.push('Roll #' + (n+1) + ': ' + rolled);
});
// do we need to keep a certain number of the rolls?
if (parsed.keep) {
outcome.original_rolls = outcome.rolls;
outcome.rolls = _.sample(outcome.original_rolls, parsed.keep);
verbose.push('Keeping ' + parsed.keep + ' of ' + parsed.times + ' rolls: ' + outcome.rolls.toString());
}
// do we need to keep the highest or lowest roll?
if (parsed.highest) {
var max = _.max(outcome.rolls);
outcome.original_rolls = outcome.original_rolls || outcome.rolls;
outcome.rolls = [ max ];
verbose.push('Selecting the highest roll: ' + max);
} else if (parsed.lowest) {
var min = _.min(outcome.rolls);
outcome.original_rolls = outcome.original_rolls || outcome.rolls;
outcome.rolls = [ min ];
verbose.push('Selecting the lowest roll: ' + min);
}
// determine the total of the rolls without the modifier
outcome.total = _.reduce(outcome.rolls, function(sum, roll) {
return sum + roll;
});
if (parsed.times > 1) {
verbose.push('Adding up all the rolls: ' + outcome.rolls.join(' + ') + ' = ' + outcome.total);
}
text.push('[ ' + outcome.rolls.join(' + ') +' ]');
// apply the multiplier
if (parsed.multiplier > 1) {
text.push('x ' + parsed.multiplier);
verbose.push('Applying the multiplier: ' + outcome.total + ' x ' + parsed.multiplier + ' = ' + (outcome.total * parsed.multiplier));
outcome.total *= parsed.multiplier;
}
// add the modifier
if (parsed.modifier > 0) {
text.push('+ ' + parsed.modifier);
verbose.push('Adding the modifier: ' + outcome.total + ' + ' + parsed.modifier + ' = ' + (outcome.total + parsed.modifier));
outcome.total += parsed.modifier;
}
verbose.push('The total of outcome #' + (n+1) + ' is ' + outcome.total);
data.outcomes.push(outcome);
if (text.length) {
data.text.push(text);
}
data.verbose.push(verbose);
});
var total = _.chain(data.outcomes).pluck('total')
.reduce(function(sum, total) {
return sum + total;
}).value();
data.verbose = _.flatten(data.verbose);
data.verbose.push('The results of ' + data.command + ' is ' + total);
if (data.text.length > 1) {
data.text = _.map(data.text, function(value, outcome) {
return '(' + value.join(' ') + ')';
}).join(' + ');
data.text += ' = ' + total;
} if (data.text.length === 0) {
data.text = total;
} else {
data.text = _.flatten(data.text).join(' ') + ' = ' + total;
}
data.text = 'The result of ' + data.command + ' is ' + data.text;
return data;
}
// parses a command given in dice notation
Dice.prototype.parse = function parse(command) {
var parsed = {};
if (typeof command !== 'string') {
throw new Error('Parameter `command` must be a string, not undefined');
}
// determine number of dice to roll
var times = command.match(/(\d+)d/i);
parsed.times = times && times[1] && parseInt(times[1]) || 1;
// determine the number of faces
var faces = command.match(/d(\d+)/i);
parsed.faces = faces && faces[1] && parseInt(faces[1]) || 20;
// determine the number of dice to keep
var keep = command.match(/\(k(\d+)\)/i);
parsed.keep = keep && keep[1] && parseInt(keep[1]) || null;
// determine if should keep the lowest rolled dice
var lowest = /-L/.test(command);
parsed.lowest = lowest;
// determine if should keep the highest rolled dice
var highest = /-H/.test(command);
parsed.highest = highest;
// determine the multiplier
var multiplier = command.match(/(?!d\d+)x(\d+)/);
parsed.multiplier = multiplier && multiplier[1] && parseInt(multiplier[1]) || 1;
// determine the modifier
var modifier = command.match(/(\+\d+\)?|-\d+)\)?/);
parsed.modifier = modifier && modifier[1] && parseInt(modifier[1]) || 0;
// determine if we need to repeat at all
var repeat = command.match(/^(\d+)x\(|\)x(\d+)$/);
parsed.repeat = repeat && repeat[1] && parseInt(repeat[1]) || repeat && repeat[2] && parseInt(repeat[2]) || 1;
return parsed;
}
// turns a parsed command into a command string
Dice.prototype.format = function format(parsed) {
var self = this;
var command = '';
if (typeof parsed === 'undefined') {
return self.options.command || 'd20';
}
// add the number of dice to be rolled
if (parsed.times) {
command += parsed.times;
}
// add the number of faces
command += (parsed.faces) ? 'd' + parsed.faces : 'd' + 20;
// add dice to keep command
if (parsed.keep) {
command += '(k' + parsed.keep + ')';
}
// add keep lowest command
if (parsed.lowest) {
command += '-L';
}
// add the multipier
if (parsed.multiplier && parsed.multiplier != '1') {
command += 'x' + parsed.multiplier;
}
// add the modifier
if (parsed.modifier && parsed.modifier > 0) {
command += '+' + parsed.modifier;
} else if (parsed.modifier) {
command += parsed.modifier;
}
// add the repeat and add command
if (parsed.repeat) {
command = parsed.repeat + '(' + command + ')';
}
return command || undefined;
}
module.exports = Dice;