Skip to content

Instantly share code, notes, and snippets.

@nyteshade
Created September 25, 2016 09:19
Show Gist options
  • Save nyteshade/c99e7c63f492e01ed9adba8642ad9430 to your computer and use it in GitHub Desktop.
Save nyteshade/c99e7c63f492e01ed9adba8642ad9430 to your computer and use it in GitHub Desktop.
Weighted Random
/**
* ChoiceSet is a weighted random solution that allows the each of the
* items that are being chosen to have simply a name or full suite of
* values other than their weights and names.
*
* Once the items have been setup with a name and weight, the system works
* by creating a one to one array. Each item in the new array is a sum of
* all the subsequent and current weights. The final weight, being the
* maximum total weight, is also stored.
*
* Finally, in order to choose a choice, in a weighted fashion, a random
* number between 0 and the maximum is chosen. The new intervals array is
* looped through and if the random number appears less than the interval
* its index is used to select the actual choice from the set. Voila.
*
* @author Brielle Harrison <[email protected]>
*/
class ChoiceSet
{
/**
* Creates a new set of choices. There is no validation in this
* constructor, however, the values supplied should be an object
* with at least two properties; name and weight. The weight can
* be any arbitrary number, floating point or integer, and the
* name should be the default String value to chosen.
*
* If a non String value is desired, an additional value property
* or any other property may be added to the choice and retrieved
* later via direct reference of the return value from .choose or
* via .chooseProp(propName)
*
* @param {Array<Object>} n-number of Objects as described above
*/
constructor(...values)
{
this.choices = values || [];
if (this.choices.length) {
this.calcIntervals();
}
}
/**
* Instantiates a ChoiceSet with numeric choices ranging from
* the supplied from value to the supplied to value. All values
* are returned with an equal weight by default.
*
* @param {Number} from a number indicating the start range
* @param {Number} to a number indicating the end range, inclusive
* @return {ChoiceSet} an instance of ChoiceSet
*/
static weightedRange(from, to)
{
let set = new ChoiceSet();
for (let i = from; i < (to + 1); i++) {
set.choices.push({
name: i,
weight: 100
});
}
set.calcIntervals();
return set;
}
/**
* An easy way to instantiate a ChoiceSet of names and values. The
* function takes an even number of parameters with the first being
* the name and the second being the weight.
*
* @param {String} name the name of the choice
* @param {Number} weight the weight of the choice
* @return {ChoiceSet} an instance of ChoiceSet
*/
static weightedSet(...values)
{
let set = new ChoiceSet();
if (values.length % 2 != 0) {
throw new Error("WeightedChoice must be instantiated with pairs");
}
set.choices = [];
for (let i = 0; i < values.length; i+=2)
{
let name = values[i];
let weight = values[i + 1];
set.choices.push({name: name, weight:weight});
}
set.calcIntervals();
return set;
}
/**
* An easy way to instantiate a ChoiceSet of names and values. The
* function takes an even number of parameters with the first being
* the name and the second being the weight.
*
* @param {String} name the name of the choice
* @param {Number} weight the weight of the choice
* @param {Object} value the value of the choice
* @return {ChoiceSet} an instance of ChoiceSet
*/
static weightedValuedSet(...values)
{
let set = new ChoiceSet();
if (values.length % 3 != 0) {
throw new Error("weightedValuedSet must be instantiated with triplets");
}
set.choices = [];
for (let i = 0; i < values.length; i+=3)
{
let name = values[i];
let weight = values[i + 1];
let value = values[i + 2];
set.choices.push({name: name, weight:weight, value: value});
}
set.calcIntervals();
return set;
}
/**
* An easy way to instantiate a ChoiceSet of names, weights and values. The
* function takes parameters in multiples of three. The first parameter is
* the name of the choice and the second is the weight. The third object is a
* collection of keys and values that will be applied to the choice in
* question.
*
* @param {String} name the name of the choice
* @param {Number} weight the weight of the choice
* @param {Object} object to be merged with the resulting choice
* @return {ChoiceSet} an instance of ChoiceSet
*/
static weightedObjectSet(...values)
{
let set = new ChoiceSet();
if (values.length % 3 != 0) {
throw new Error("weightedObjectSet must be instantiated with triplets");
}
set.choices = [];
for (let i = 0; i < values.length; i+=3)
{
let name = values[i];
let weight = values[i + 1];
let object = values[i + 2];
set.choices.push({name: name, weight: weight, _obj: object});
}
set.calcIntervals();
return set;
}
/**
* Calculates the intervals of the weights of the choices in the set. It
* also determines the maximum total weight in the set.
*/
calcIntervals()
{
let intervals = [];
this.choices.reduce(
function(p, c) {
intervals.push(
((p && p.weight) || 0) +
((c && c.weight) || 0)
);
},
null
);
intervals = intervals.map(function(cur, idx, array) {
return cur + array.slice(0,idx).reduce((p, c) => p + c, 0);
});
this.intervals = intervals;
this.maxInterval = intervals[intervals.length - 1];
}
/**
* It randomly choose one item from the set. It does so based on a
* randomly chosen number within the given weight set.
*
* @param {String} prop the property of the randomly chosen item
* @return {Mixed} the property value specified for the chosen item. This
* defaults to 'name'.
*/
chooseOne(prop = 'name')
{
return this.chooseProp(prop);
}
/**
* Returns an array of results equivalent to those returned by
* chooseOne.
*
* @param {Number} count an integer denoting the number of choices to pick
* @param {String} prop the property of the randomly chosen item
* @return {Mixed} the property value specified for the chosen item. This
* defaults to 'name'.
*/
chooseSome(count, prop = 'name') {
let choices = [];
for (let i = 0; i < count; i++) {
choices.push(this.chooseProp(prop))
}
return choices;
}
/**
* Simulates rolling dice with n-number of sides. In pen and paper
* role-playing games, 3d6 means to roll three six sided dice together
* and sum their results. Calling ChoiceSet.rollDice(3, 6) will simulate
* the same effect.
*
* Optionally, if repeat is set to a number greater than 1, an array of
* values is returned with the repeated results numbering the supplied
* repeat count.
*
* @param {Number} times the number of times the die should be rolled
* @param {Number} sides the number of sides the die should have
* @param {Number} repeat the number of times the whole process should be
* repeated
* @return {Mixed} either an array of Numbers or a single Number resulting
* in the sum of a die with sides rolled times times.
*/
static rollDice(times, sides, repeat = 1, dropLowest = 0) {
let count = [];
let die = ChoiceSet.weightedRange(1, sides);
for (let i = 0; i < repeat; i++) {
let rolls = die.chooseSome(times);
rolls.sort();
if (dropLowest) {
rolls = rolls.slice(dropLowest);
}
count.push(rolls.reduce((p,c) => p + c, 0));
}
return repeat === 1 ? count[0] : count;
}
/**
* Randomly chooses a value from the set and returns the specified property
* from the chosen object.
*
* @return {String} key the property value specified for the chosen item.
* This defaults to 'name'.
*/
chooseProp(key = 'name')
{
let choice = this.choose;
return (choice._obj && choice._obj[key]) || choice[key];
}
/**
* Chooses a value via .choose and then retrieves the value property of
* the choice.
*
* @return {Mixed} the value of the chosen item from the set
*/
get chooseValue()
{
let choice = this.choose;
return choice.value || choice._obj || choice.name;
}
/**
* Randomly chooses a value from the set and returns it in its entirety.
*
* @return {Mixed} an object from the ChoiceSet.
*/
get choose()
{
let roll = Math.random() * this.maxInterval;
let item = null;
let index = -1;
for (let i = 0; i < this.intervals.length; i++) {
if (roll < this.intervals[i]) {
index = i;
break;
}
}
if (index === -1) {
console.log('ERROR');
console.log(roll, index);
}
return this.choices[index];
}
}
module.exports = ChoiceSet;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment