Reputation: 678
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
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:
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
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
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
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