1

I have this:

var users = [user1, user2, user3];
var chances = [20, 20, 60];
var total = 100;
var random = Math.floor((Math.random() * total) + 1);

if (random > 40 && random < 100) {
    console.log('winner:', users[2]);
}

if (random > 20 && random < 40) {
    console.log('winner:', users[1]);
}

if (random > 0 && random < 20) {
    console.log('winner:', users[0]);
}

This will give every user a fair chance to win. (60 has a 60% chance, 20 has a 20% chance).

But what I actually need is that this is a dynamic for each (or anything else) function.

Example of my thoughts:

 chances.forEach(function(entry) {
     if (unknownValue > unkownValue2 && unknownValue3 < unknownValue4) {
         console.log('winner:', unknownUser);
     };
 });

So basically, if the value for the chances array are 50, 100 and 20 the chance that number 100 wins must be 2x higher than 50, and 5x higher than 20.

Im happy for every answer and please do not mark this as duplicate for slot machines percentages, this is NOT what i need.

4 Answers4

10

As an alternative to the probability solutions, you could also create an array with duplicate users so that their count corresponds to the associated percentage. It is then required that your percentages are positive integers, not fractions.

Like this:

const users = [
  {name: "Jim",   pct: 20},
  {name: "Helen", pct: 20},
  {name: "Anna",  pct: 60}
];

const expanded = users.flatMap(user => Array(user.pct).fill(user));
const winner = expanded[Math.floor(Math.random() * expanded.length)];
console.log("winner: " + winner.name);

The numbers don't really have to be percentages. If you need more precision, just use bigger numbers that maybe add up to 1000 or 10000.

trincot
  • 317,000
  • 35
  • 244
  • 286
  • Hey, really awesome answer! Just to clarify, the numbers sum of the pct's doesnt need to be 1, 100, or 1000? I mean, 12,43,89 would also work right? – Alejandro Castro Jun 07 '18 at 11:39
  • 1
    Yep, the total is used (with `expanded.length`) and so the random number that is generated will be picked up to that limit, whatever it may be. The only limitation is that they should not have fractions and should not be astronomically big, since this solution temporarily allocates memory proportionate to those numbers. – trincot Jun 07 '18 at 11:40
2

Transform ratios into percentages. Apparently, their sum should account for 1, and value for each is val/total:

function transformRatiosToAccPercentages(ratios) {
  const total = ratios.reduce((sum, el) => sum += el, 0);
  let acc = 0;
  const accPercentages = ratios.map(rat => acc += rat / total);
  return accPercentages;
}

function chooseBiasedRandom(accPercentages) {
  const random = Math.random();
  const index = accPercentages.findIndex(acc => random < acc);
  return index;
}

// And that's how it can be used:

const users = {
  Alpha: 50,
  Bravo: 100,
  Charlie: 10
};

const userNames = Object.keys(users); 
const ratios = Object.values(users);

const attempts = 1E6;
const counter = Array(userNames.length).fill(0);

const accPercentages = transformRatiosToAccPercentages(ratios);

for (let i = 1; i <= attempts; i++) {
  const index = chooseBiasedRandom(accPercentages);
  counter[index]++;
  // console.log(`Attempt ${i}: user ${userNames[index]} wins!`);
}

console.log(counter);
raina77ow
  • 103,633
  • 15
  • 192
  • 229
1

    //put chances and user object in the same object, in an array
    let userChances = [
        {userObj: 'user1', chance: 20},
        {userObj: 'user2', chance: 40},
        {userObj: 'user2', chance: 60}
    ];
    //we loop the items and turn the chance into a running total...
    for (let i = 0; i < userChances.length; i++) {
        if (i > 0) {
            userChances[i] = {
                userObj: userChances[i].userObj,
                chance: userChances[i].chance + userChances[i - 1].chance
            };
        }
    }
    //now data looks like this:
    //let userChances = [
    //    {userObj: user1, chance: 20},
    //    {userObj: user2, chance: 60},
    //    {userObj: user2, chance: 120}
    //];
    //we get the total available chance, which is now the value of the chance property of the last element in the array
    let totalChances = userChances[userChances.length - 1].chance;
    let yourChance = Math.floor((Math.random() * totalChances) + 1);
    //loop one last time
    for (let i= 0; i < userChances.length; i ++) {
        //if our number is less than the current user chance, we found the user! return it! otherwise, proceed to check the next user...
        if (yourChance <= userChances[i].chance) {
            console.log('Winner', userChances[i].userObj);
            break;
        }
        //please note that if this does not return or break the loop, every user after this will be logged as a winner too!
    }
Fabio Lolli
  • 859
  • 7
  • 23
1

You could use an array with probabilities, and check and count against a random value.

This function first sets the return value to the last possible index, and then iterates over until the rest of the random value is smaller than the actual probability. Also, sum of all probabilities should be equal to one.

For implementation, you just need to take the function for getting an index for the array of users.

var users = ['user1', 'user2', 'user3'],
    probabilities = [0.2, 0.2, 0.6],
    selectedUser = users[getRandomIndexByProbability(probabilities)];

Then, the code shows the distribution of indices.

function getRandomIndexByProbability(probabilities) {
    var r = Math.random(),
        index = probabilities.length - 1;

    probabilities.some(function (probability, i) {
        if (r < probability) {
            index = i;
            return true;
        }
        r -= probability;
    });
    return index;
}

var i,
    probabilities = [0.2, 0.2, 0.6],
    count = {},
    index;

probabilities.forEach(function (_, i) { count[i] = 0; });

for (i = 0; i < 1e6; i++) {
    index = getRandomIndexByProbability(probabilities);
    count[index]++;
}

console.log(count);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Emma
  • 27,428
  • 11
  • 44
  • 69
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392