按照随机百分比的几率执行JavaScript函数

4

假设我想按照百分比的机会来触发一个函数

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

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

现在我希望添加更多的功能来触发事件,我所做的是:
//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();
}

但这会使得 A()B()C() 之前优先执行。因此我将代码改为:
//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(); }

这种方法看起来是公平的,但因为需要在每个if else函数中硬编码机会,所以效率很低,并且如果我的功能和触发机会列表很长,我将需要使if else变得非常冗长和混乱。
问题是有没有更好、更高效的方法?不像上面展示的这两种方法。
*此外,公平而公正也很重要(听起来像一个游戏)
抱歉如果我表达不清楚,在此方面的任何帮助都将不胜感激。谢谢。

2
你可以省略 && 前面的条件,因为它们总是为真。 - Ryan Searle
@RyanSearle 很好的观点,谢谢指出。 - Dean
如果您有太多的函数,它们的概率加起来超过了1.0怎么办?那该如何处理呢? - Sergio Tulentsev
@SergioTulentsev 好的,那在我的情况下就是声明问题了,只要假装所有函数的机会总和不超过100%即可。 - Dean
4个回答

3
你可以创建一个包含你想调用的函数及其被调用概率的列表。然后,使用这些概率将随机数的范围分成多个区间。在这个例子中,它可能是这样的:
0.0         0.5           0.6           0.7
     0.5/A         0.1/B         0.1/C

这些条目不需要根据机会进行排序。如果机会的总和大于1.0,则数组的最后一个元素将不被调用。这是您需要使用此方法自己确保的内容。

代码可能像这样:

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)


2
这种方法看起来很公平,但是它看起来效率低下,因为它需要在每一个if else中硬编码机会。虽然如此,这是正确的做法。此外,使用数学运算使它看起来比实际情况复杂。
var randNumber = Math.random();
if (randNumber      <= 0.5) { A(); }
else if (randNumber <= 0.6) { B(); }
else if (randNumber <= 0.7) { C(); }

如果它们之间的间隔是固定的,您可以避免那些硬编码的值:

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(); }

或者实际上,我想即使它不是:

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

另一种方法是将其转换为可靠地以可预测字符串¹(例如0-9)结尾的数字,并使用调度对象:

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();
}

请注意,尽管上面使用数字字面量编写,但是dispatch对象的属性名称实际上是字符串("0""1"等)。
使用列表的t.niese's approach比使用此调度对象更好。

基本上,这意味着避免(某些)分数值并保持在合理的数字范围内。例如,如果您进行了类似于 0.009 / 3 的计算,并且(合理地)期望能够使用 "0.003" 作为您的键,那么它将不起作用,因为 String(0.009 / 3)"0.0029999999999999996",而不是 "0.003",这是由于 IEEE-754 双精度二进制浮点不精确性造成的。(而 0.09 / 3 则没有问题,给我们 "0.03")。


我喜欢你在派发对象上的处理方式。只是挑剔一点,如果百分比几率是一个“整数”,那么它将非常有效,但如果我的百分比几率是“0.2%”,那么我需要在1-1000之间随机化,并且我必须将“10%”函数值放置100次? - Dean
1
应该是var f = dispatch[Math.floor(Math.random() * 10)];吧?用括号而不是圆括号。 - duatis
@duatis:谢谢,打字错误已经修复。 :-) - T.J. Crowder
@Dean - 我不完全确定我理解你的意思,但听起来你最终会得到一个具有1000个条目的“dispatch”对象。这很好,尽管可能不太方便。您必须小心避免使用分数时出现不精确的问题。 - T.J. Crowder

2
这是我构建布局的方式。
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);

1

让我们分成几个部分:

1)范围 创建一个包含所有范围上限的数组,例如,假设您想要以下范围0-10、11-20和21-100,那么数组将是:

const arr = [10, 20, 100]

为了找到正确的范围:

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;
    }
}

现在我们知道了randNumber所处的范围。

2) 现在我们将找到一种映射范围与应该触发的函数的方法。

最简单的方法是创建另一个数组,其中包含与它们相应的范围上限相同索引处的函数,并且 - 对于前面的示例,如果:

  • 0到10 - 函数A()
  • 11到20 - 函数B()
  • 21到100 - 函数C()

我们将得到

const func = [A, B, C]

为了保存“范围索引”并知道应该触发哪个函数,我将在以下代码中使用变量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}`);
}

希望这有所帮助。

你的例子很好,非常感谢。但是有一些错误,应该是 i < arr.length && !rangeFound 而不是 rangeFound,应该是 Math.random() * 100 而不是 Math.random,否则只能得到第一个函数。 - Dean

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接