Dean
Dean

Reputation: 678

Javascript execute function by random percentage chances

lets say, I would like to trigger a function by percentage chance

function A () { console.log('A triggered'); } //50% chance to trigger

if (Math.random() >= 0.5) A();

now i would like to add in more function to be trigger by chances, what i did was

//method 1
function B () { console.log('B triggered'); } //10% chance to trigger
function C () { console.log('C triggered'); } //10% chance to trigger

if (Math.random() >= 0.5) {
    A();
} else if (Math.random() > (0.5 + 0.1)) {
    B();
} else if (Math.random() > (0.5 + 0.1 + 0.1)) {
    C();
}

But this make A() get priority before B() and C(). Therefore I change the code into

//method 2
var randNumber = Math.random();

if (randNumber <= 0.5) { A(); }
else if (randNumber > 0.5 && randNumber <= (0.5 + 0.1)) { B(); }
else if (randNumber > (0.5 + 0.1) && randNumber <= (0.5 + 0.1 + 0.1)) { C(); }

This method looks fair but it looks inefficient because it need to be hardcoded chances in every single if else function and if I have a list long of function and triggering chances, I will need to make the if else very long and messy

Question is is there any method I can done this better and efficient? unlike these 2 method i shown above.

*also fair and square is important as well (sound like a game)

Sorry if I explain the situation badly, Any help to this will be appreciated. Thanks.

Upvotes: 4

Views: 4906

Answers (4)

Rochadsouza
Rochadsouza

Reputation: 978

Let's break this into parts:

1) Ranges Create an array with all ranges' upper bound, for example, lets say that you want the following ranges 0 - 10, 11 - 20 and 21 to 100 so the array will be:

const arr = [10, 20, 100]

In order to find the correct range:

let randNumber = Math.random() * 100;
let rangeFound = false;

for (i = 0; i < arr.length && !rangeFound; i++) { 

    let lowerBound = i>0?arr[i-1]:-1,
        upperBound = arr[i];

    if(randNumber > lowerBound && randNumber <= upperBound){
        //save a reference to the current range (index?)
        rangeFound = true;
    }
}

At this point we know in which range our randNumber is.

2) Now we will get a way to map the range with the function that should be triggered.

The simplest way is to create another array with functions placed in the same index as their corresponding range upper bound and - to the previous example, if:

  • 0 to 10 - function A()
  • 11 to 20 - function B()
  • 21 to 100 - function C()

We will get

const func = [A, B, C]

Ir order to save the "range index" and know which function should be triggered, I will use in the following code the variable rangeIndex

function A() { /*do something A*/}
function B() { /*do something B*/}
function C() { /*do something C*/}


    const arr = [10, 20, 100];
const func = [A, B, C];

let randNumber = Math.random() * 100;
let rangeFound = false;
let rangeIndex = -1;

for (i = 0; i < arr.length && !rangeFound; i++) { 

    let lowerBound = i>0?arr[i-1]:-1,
        upperBound = arr[i];

    if(randNumber > lowerBound && randNumber <= upperBound){
        rangeIndex = i;
        rangeFound = true;
    }
}

if(rangeFound){
    let f = func[rangeIndex];
    f();
} else {
    console.warn(`There is no suitable range for ${randNumber}`);
}

Hope this helps.

Upvotes: 1

Ryan Searle
Ryan Searle

Reputation: 1627

This is how I would go about laying it out.

let outcomes = [
    { chance: 50, action:() => { console.log('A'); } },
    { chance: 10, action:() => { console.log('B'); } },
    { chance: 10, action:() => { console.log('C'); } }
]

function PerformAction(actions){
    let totalChance = actions.reduce((accum, current)=>{
        return accum + current.chance;
    }, 0);

    if(totalChance > 100){
        throw Error('Total chance cannnot exceed 100%');
    }

    if(totalChance < 100) {
        actions.push({
            chance: 100 - totalChance,
            action: function(){
                console.log('No action taken');
            }
        })
    }

    let outcome = Math.random() * 100;

    actions.find((current)=>{        
        outcome = outcome - current.chance;
        return outcome <= 0;
    }).action();
}

PerformAction(outcomes);

Upvotes: 2

T.J. Crowder
T.J. Crowder

Reputation: 1075885

This method looks fair but it looks inefficient because it need to be hardcoded chances in every single if else

That's true, though the lower bound is unnecessary, since you're using else. Also, using the math makes it look more complicated than it is:

var randNumber = Math.random();
if (randNumber      <= 0.5) { A(); }
else if (randNumber <= 0.6) { B(); }
else if (randNumber <= 0.7) { C(); }

If the interval between them is fixed, you can avoid those hardcoded values:

var randNumber = Math.random();
var chance = 0.5;
var interval = 0.1;
if (randNumber      <= chance)               { A(); }
else if (randNumber <= (chance += interval)) { B(); }
else if (randNumber <= (chance += interval)) { C(); }

Or actually, I suppose even if it isn't:

var randNumber = Math.random();
var chance = 0.5;
if (randNumber      <= chance)          { A(); }
else if (randNumber <= (chance += 0.1)) { B(); }
else if (randNumber <= (chance += 0.2)) { C(); } // Intentionally different from
                                                 // yours to show different spacing

Another way to do it would be to convert to only numbers that will reliably end up as predictable strings¹ (say, 0-9) and use a dispatch object:

var dispatch = {
    0: A,
    1: A,
    2: A,
    3: A,
    4: A,
    5: B,
    6: C
};
var f = dispatch[Math.floor(Math.random() * 10)];
if (f) {
    f();
}

Note that although written with numeric literals above, the names of the properties of the dispatch object are actually strings ("0", "1", etc.).

t.niese's approach with a list is better than this dispatch object, though.


¹ Basically this means avoiding (some) fractional values and staying in a reasonable numeric range. For instance, if you had a calculation that ended up doing something like 0.009 / 3 and you (reasonably) expected to be able to use "0.003" as your key, it wouldn't work, because String(0.009 / 3) is "0.0029999999999999996", not "0.003", thanks to IEEE-754 double-precision binary floating point imprecision. (Whereas 0.09 / 3 is fine, gives us "0.03").

Upvotes: 2

t.niese
t.niese

Reputation: 40882

You could create a list of the functions you want to call and their chance to be called. Then you divided the range of the random numbers into chunks using the chances. In this example it would be:

0.0         0.5           0.6           0.7
     0.5/A         0.1/B         0.1/C

The entries do not need to be sorted based on the chances. If the sum of the chances is larger then 1.0 then the last elements of the array won't be called. That's something you need to ensure yourself with this approach.

The code could look like this:

function A() {
  console.log('A was called')
}

function B() {
  console.log('B was called')
}

function C() {
  console.log('C was called')
}


var list = [
  {chance: 0.5, func: A},
  {chance: 0.1, func: B},
  {chance: 0.1, func: C}
];

function callRandomFunction(list) {
  var rand = Math.random() // get a random number between 0 and 1
  var accumulatedChance = 0 // used to figure out the current

  var found = list.find(function(element) { // iterate through all elements 
    accumulatedChance += element.chance // accumulate the chances
    return accumulatedChance >= rand // tests if the element is in the range and if yes this item is stored in 'found'
  })

  if( found ) {
    console.log('match found for: ' + rand)
    found.func()
  } else {
    console.log('no match found for: ' + rand)
  }
}

callRandomFunction(list)

Upvotes: 3

Related Questions