【问题标题】:Generating Random Numbers for RPG games为 RPG 游戏生成随机数
【发布时间】:2016-11-19 13:58:09
【问题描述】:

我想知道是否有一种算法可以生成随机数,该随机数很可能在从最小值到最大值的范围内都很低。例如,如果您生成一个介于 1 和 100 之间的随机数,如果您使用 f(min: 1, max: 100, avg: 30) 调用该函数,那么它大部分时间应该低于 30,但如果您使用 f(min: 1, max: 200, avg: 10) 调用它,则平均值应该是 10。很多的游戏可以做到这一点,但我根本找不到用公式来做到这一点的方法。我见过的大多数例子都使用“drop table”或类似的东西。

我想出了一种相当简单的方法来衡量掷骰结果的权重,但效率不高,而且您对它没有太多控制权

var pseudoRand = function(min, max, n) {
    if (n > 0) {
        return pseudoRand(min, Math.random() * (max - min) + min, n - 1)
    }

    return max;
}

rands = []
for (var i = 0; i < 20000; i++) {
    rands.push(pseudoRand(0, 100, 1))
}

avg = rands.reduce(function(x, y) { return x + y } ) / rands.length
console.log(avg); // ~50

该函数只是在 min 和 max N 次之间选择一个随机数,每次迭代都会更新最后一次滚动的最大值。因此,如果您使用N = 2, and max = 100 调用它,那么它必须连续两次滚动 100 才能返回 100

我查看了维基百科上的一些分布,但我不太了解它们,无法知道如何控制最小和最大输出等。

非常欢迎任何帮助

【问题讨论】:

  • 期望的中位数是多少?

标签: algorithm random


【解决方案1】:

嗯,根据我对您的问题的了解,我希望解决方案满足以下标准:

a) 属于单一分布:如果我们需要在每个函数调用中多次“滚动”(调用 math.Random),然后聚合或丢弃一些结果,则它不再根据给定函数真正分布。

b) 不是计算密集型的:一些解决方案使用积分(Gamma 分布,高斯分布),而那些是计算密集型的。在您的描述中,您提到您希望能够“使用公式计算它”,这符合此描述(基本上,您需要一个 O(1) 函数)。

c) 相对“分布良好”,例如没有波峰和波谷,但大多数结果都集中在平均值附近,并且具有良好的可预测斜率向下朝向末端,但最小值和最大值不为零的概率。

d) 不需要像在删除表中那样在内存中存储大数组。

我觉得这个功能符合要求:

    var pseudoRand = function(min, max, avg )
    {
        var randomFraction = Math.random();
        var head = (avg - min);
        var tail = (max - avg);
        var skewdness = tail / (head + tail);
        if (randomFraction < skewdness)
            return min + (randomFraction / skewdness) * head;
        else
            return  avg + (1 - randomFraction) / (1 - skewdness) * tail;
    }

这将返回浮点数,但您可以通过调用轻松将它们转换为整数

(int) Math.round(pseudoRand(...))

它在我所有的测试中都返回了正确的平均值,并且它也很好地分布在了末端。希望这可以帮助。祝你好运。

【讨论】:

    【解决方案2】:

    看了很多好的解释和一些好的想法,我仍然认为这可以帮助你:

    您可以在0 周围采用任何分布函数f,并将您感兴趣的区间替换为您想要的区间[1,100]f -> f '

    然后将 f' 的结果提供给 C++ discrete_distribution

    我在下面有一个正态分布的例子,但我无法将我的结果输入到这个函数中:-S

    #include <iostream>
    #include <random>
    #include <chrono>
    #include <cmath>
    
    
    using namespace std;
    
    
    double p1(double x, double mean, double sigma); // p(x|x_avg,sigma)
    double p2(int x, int x_min, int x_max, double x_avg, double z_min, double z_max); // transform ("stretch") it to the interval
    int plot_ps(int x_avg, int x_min, int x_max, double sigma);
    
    int main()
    {
        int x_min = 1;
        int x_max = 20;
        int x_avg = 6;
    
        double sigma = 5;
    
        /*
        int p[]={2,1,3,1,2,5,1,1,1,1};
    
        default_random_engine generator (chrono::system_clock::now().time_since_epoch().count());
        discrete_distribution<int> distribution {p*};
    
        for (int i=0; i< 10; i++)
            cout << i << "\t" << distribution(generator) << endl;
        */
        plot_ps(x_avg, x_min, x_max, sigma);
    
        return 0; //*/
    }
    
    // Normal distribution function
    double p1(double x, double mean, double sigma)
    {
        return 1/(sigma*sqrt(2*M_PI))
             * exp(-(x-mean)*(x-mean) / (2*sigma*sigma));
    }
    
    // Transforms intervals to your wishes ;)
    // z_min and z_max are the desired values f'(x_min) and f'(x_max)
    double p2(int x, int x_min, int x_max, double x_avg, double z_min, double z_max)
    {
        double y;
        double sigma = 1.0;
        double y_min = -sigma*sqrt(-2*log(z_min));
        double y_max =  sigma*sqrt(-2*log(z_max));
        if(x < x_avg)
            y = -(x-x_avg)/(x_avg-x_min)*y_min;
        else
            y = -(x-x_avg)/(x_avg-x_max)*y_max;
        return p1(y, 0.0, sigma);
    }
    
    //plots both distribution functions
    int plot_ps(int x_avg, int x_min, int x_max, double sigma)
    {
        double z = (1.0+x_max-x_min);
    
        // plot p1
        for (int i=1; i<=20; i++)
        {
            cout << i << "\t" <<
            string(int(p1(i, x_avg, sigma)*(sigma*sqrt(2*M_PI)*20.0)+0.5), '*')
            << endl;
        }
    
        cout << endl;
    
        // plot p2
        for (int i=1; i<=20; i++)
        {
            cout << i << "\t" <<
            string(int(p2(i, x_min, x_max, x_avg, 1.0/z, 1.0/z)*(20.0*sqrt(2*M_PI))+0.5), '*')
            << endl;
        }
    }
    

    如果我让他们绘图,结果如下:

    1   ************
    2   ***************
    3   *****************
    4   ******************
    5   ********************
    6   ********************
    7   ********************
    8   ******************
    9   *****************
    10  ***************
    11  ************
    12  **********
    13  ********
    14  ******
    15  ****
    16  ***
    17  **
    18  *
    19  *
    20  
    
    1   *
    2   ***
    3   *******
    4   ************
    5   ******************
    6   ********************
    7   ********************
    8   *******************
    9   *****************
    10  ****************
    11  **************
    12  ************
    13  *********
    14  ********
    15  ******
    16  ****
    17  ***
    18  **
    19  **
    20  *
    

    所以 - 如果您可以将此结果提供给 discrete_distribution&lt;int&gt; distribution {},那么您将得到您想要的一切...

    【讨论】:

      【解决方案3】:

      概率分布函数只是一个函数,当您输入一个值 X 时,它将返回获得该值 X 的概率。累积分布函数是获得一个小于或等于 X 的数字的概率。A CDF 是 PDF 的积分。 CDF 几乎总是一对一的函数,所以它几乎总是有一个逆函数。

      要生成 PDF,请在 x 轴上绘制值,在 y 轴上绘制概率。所有概率的总和(离散)或积分(连续)应加起来为 1。找到一些可以正确模拟该方程的函数。为此,您可能需要查找一些 PDF。

      基本算法

      https://en.wikipedia.org/wiki/Inverse_transform_sampling

      此算法基于逆变换采样。 ITS 背后的想法是,您在 CDF 的 y 轴上随机选择一个值并找到它对应的 x 值。这是有道理的,因为越有可能随机选择一个值,它在 CDF 的 y 轴上占用的“空间”就越大。

      1. 想出一些概率分布公式。例如,如果您希望数字越高,被选中的几率越大,您可以使用 f(x)=x 或 f(x)=x^2 之类的东西。如果您想要中间凸起的东西,您可以使用高斯分布或 1/(1+x^2)。如果您需要有界公式,可以使用 Beta 分布或 Kumaraswamy 分布。
      2. 集成 PDF 以获得累积分布函数。
      3. 求 CDF 的逆。
      4. 生成一个随机数并将其插入 CDF 的逆。
      5. 将结果乘以 (max-min),然后加上 min
      6. 将结果四舍五入为最接近的整数。

      步骤 1 到 3 是您必须硬编码到游戏中的内容。对于任何 PDF,解决它的唯一方法是求解与其均值相对应的形状参数,并遵守您想要的形状参数的约束。如果您想使用 Kumaraswamy 分布,您需要将其设置为形状参数 a 和 b 始终大于 1。

      我建议使用 Kumaraswamy 分布,因为它是有界的,并且它有一个非常好的封闭形式和封闭形式的逆。它只有两个参数,a 和 b,并且非常灵活,因为它可以模拟许多不同的场景,包括多项式行为、钟形曲线行为和在两个边缘都有峰值的盆状行为。此外,使用此功能进行建模并不难。形状参数b越高,越向左倾斜,形状参数a越高,越向右倾斜。如果 a 和 b 都小于 1,则分布看起来像一个槽或盆。如果 a 或 b 等于 1,则分布将是一个不会将凹度从 0 更改为 1 的多项式。如果 a 和 b 都等于 1,则分布是一条直线。如果 a 和 b 大于 1,则函数看起来像钟形曲线。学习这一点的最佳方法是实际绘制这些函数的图表,或者只运行逆变换采样算法。

      https://en.wikipedia.org/wiki/Kumaraswamy_distribution

      例如,如果我想得到一个这样的概率分布,其中 a=2 和 b=5 从 0 到 100:

      https://www.wolframalpha.com/input/?i=2*5*x%5E(2-1)*(1-x%5E2)%5E(5-1)+from+x%3D0+to+x%3D1

      它的 CDF 是:

      CDF(x)=1-(1-x^2)^5

      它的倒数是:

      CDF^-1(x)=(1-(1-x)^(1/5))^(1/2)

      Kumaraswamy 分布的一般逆是: CDF^-1(x)=(1-(1-x)^(1/b))^(1/a)

      然后我会生成一个从 0 到 1 的数字,将其放入 CDF^-1(x),然后将结果乘以 100。

      优点

      • 非常准确
      • 连续,不谨慎
      • 使用一个公式,空间非常小
      • 让您可以充分控制随机性的确切分布方式
      • 其中许多公式的 CDF 带有某种逆函数
      • 有一些方法可以在两端绑定函数。例如,Kumaraswamy 分布的范围是 0 到 1,因此您只需输入一个介于 0 和 1 之间的浮点数,然后将结果乘以 (max-min) 并加上 min。 Beta 分布的界限因您传递给它的值而异。对于 PDF(x)=x,CDF(x)=(x^2)/2,因此您可以生成从 CDF(0) 到 CDF(max-min) 的随机值。

      缺点

      • 您需要提出您计划使用的确切分布及其形状
      • 您计划使用的每一个通用公式都需要硬编码到游戏中。换句话说,您可以将一般的 Kumaraswamy 分布编程到游戏中,并拥有一个根据分布及其参数 a 和 b 生成随机数的函数,但不是一个根据平均值为您生成分布的函数。如果您想使用 Distribution x,则必须找出最适合您想要查看的数据的 a 和 b 值,并将这些值硬编码到游戏中。

      【讨论】:

      • 这正是我想要的。谢谢
      【解决方案4】:

      您可以组合 2 个随机过程。例如:

      first rand R1 = f(min: 1, max: 20, avg: 10); 第二个 rand R2 = f(min:1, max : 10, avg : 1);

      然后将 R1*R2 相乘,得到 [1-200] 之间的结果,平均值约为 10(平均值会偏移一点)

      另一种选择是找到您要使用的随机函数的逆函数。此选项必须在程序启动时初始化,但不需要重新计算。这里使用的数学可以在很多数学库中找到。我将通过一个未知随机函数的例子来逐点解释,其中只有四个点是已知的:

      1. 首先,使用 3 阶或更高阶的多项式函数拟合四点曲线。
      2. 然后您应该有一个类型为参数化的函数:ax+bx^2+cx^3+d。
      3. 求函数的不定积分(积分形式为a/2x^2+b/3x^3+c/4x^4+dx,我们称之为quarticEq)。
      4. 计算多项式从最小值到最大值的积分。
      5. 取一个 0-1 之间的均匀随机数,然后乘以步骤 5 中计算的积分值。(我们将结果命名为“R”)
      6. 现在求解方程 R = quarticEq 的 x。

      希望最后一部分是众所周知的,并且您应该能够找到可以进行此计算的库(请参阅wiki)。如果积分函数的逆函数没有封闭形式的解(如任何五次或更高阶的一般多项式),您可以使用求根方法,例如Newton's Method

      这种计算可用于创建任何类型的随机分布。

      编辑:

      您可以在wikipedia 中找到上述逆变换采样,而我在implementation 中找到了这个(我还没有尝试过。)

      【讨论】:

        【解决方案5】:

        我会为此使用一个简单的数学函数。根据您的描述,您需要像 y = x^2 这样的指数级数。平均而言(平均值为 x=0.5,因为 rand 给你一个从 0 到 1 的数字)你会得到 0.25。如果你想要一个更低的平均数,你可以使用更高的指数,比如 y = x^3 这将导致 y = 0.125 在 x = 0.5 例子: http://www.meta-calculator.com/online/?panel-102-graph&data-bounds-xMin=-2&data-bounds-xMax=2&data-bounds-yMin=-2&data-bounds-yMax=2&data-equations-0=%22y%3Dx%5E2%22&data-rand=undefined&data-hideGrid=false

        PS:我调整了函数来计算所需的指数以获得平均结果。 代码示例:

        function expRand (min, max, exponent) {
            return Math.round( Math.pow( Math.random(), exponent) * (max - min) + min);
        }
        
        function averageRand (min, max, average) {
            var exponent = Math.log(((average - min) / (max - min))) / Math.log(0.5);
            return expRand(min, max, exponent);
        }
        
        alert(averageRand(1, 100, 10));
        

        【讨论】:

          【解决方案6】:

          试试这个, 为低于平均值的数字范围生成一个随机数,并为高于平均值的数字范围生成第二个随机数。

          然后随机选择其中一个,每个范围将被选择 50% 的时间。

          var psuedoRand = function(min, max, avg) {
            var upperRand = (int)(Math.random() * (max - avg) + avg);
            var lowerRand = (int)(Math.random() * (avg - min) + min);
          
            if (math.random() < 0.5)
              return lowerRand;
            else
              return upperRand;
          }
          

          【讨论】:

          • 注意:在 if 语句中增加十进制数“0.5”会增加选择低于“avg”的数字的频率,反之亦然。
          【解决方案7】:
          private int roll(int minRoll, int avgRoll, int maxRoll) {
              // Generating random number #1
              int firstRoll = ThreadLocalRandom.current().nextInt(minRoll, maxRoll + 1);
          
              // Iterating 3 times will result in the roll being relatively close to
              // the average roll.
              if (firstRoll > avgRoll) {
                  // If the first roll is higher than the (set) average roll:
                  for (int i = 0; i < 3; i++) {
                      int verificationRoll = ThreadLocalRandom.current().nextInt(minRoll, maxRoll + 1);
          
                      if (firstRoll > verificationRoll && verificationRoll >= avgRoll) {
                          // If the following condition is met:
                          // The iteration-roll is closer to 30 than the first roll
                          firstRoll = verificationRoll;
                      }
                  }
              } else if (firstRoll < avgRoll) {
                  // If the first roll is lower than the (set) average roll:
                  for (int i = 0; i < 3; i++) {
                      int verificationRoll = ThreadLocalRandom.current().nextInt(minRoll, maxRoll + 1);
          
                      if (firstRoll < verificationRoll && verificationRoll <= avgRoll) {
                          // If the following condition is met:
                          // The iteration-roll is closer to 30 than the first roll
                          firstRoll = verificationRoll;
                      }
                  }
              }
              return firstRoll;
          }
          

          说明:

          • 滚动
          • 检查滚动是否高于、低于或正好 30
          • 如果高于,则重新掷骰 3 次并根据新掷骰设置掷骰,如果低于但 >= 30
          • 如果低于,重新滚动 3 次并根据新滚动设置滚动,如果 更高但
          • 如果正好是 30,则不要重新设置卷轴
          • 归还卷

          优点:

          • 简单
          • 有效
          • 表现不错

          缺点:

          • 30-40 范围内的结果自然会多于 20-30 范围内的结果,这很简单,因为 30-70 的关系。

          测试:

          您可以将以下方法与roll()-方法结合使用来进行测试。数据保存在 hashmap 中(将数字映射到出现次数)。

          public void rollTheD100() {
          
              int maxNr = 100;
              int minNr = 1;
              int avgNr = 30;
          
              Map<Integer, Integer> numberOccurenceMap = new HashMap<>();
          
              // "Initialization" of the map (please don't hit me for calling it initialization)
              for (int i = 1; i <= 100; i++) {
                  numberOccurenceMap.put(i, 0);
              }
          
              // Rolling (100k times)
              for (int i = 0; i < 100000; i++) {
                  int dummy = roll(minNr, avgNr, maxNr);
                  numberOccurenceMap.put(dummy, numberOccurenceMap.get(dummy) + 1);
              }
          
              int numberPack = 0;
          
              for (int i = 1; i <= 100; i++) {
                  numberPack = numberPack + numberOccurenceMap.get(i);
                  if (i % 10 == 0) {
                      System.out.println("<" + i + ": " + numberPack);
                      numberPack = 0;
                  }
              }
          }
          

          结果(100.000 卷):

          这些都符合预期。请注意,您始终可以微调结果,只需修改 roll()-方法中的迭代计数(平均值应越接近 30,应包含的迭代越多(请注意,这可能会损害性能一定程度))。另请注意,到目前为止,30 是(如预期的那样)出现次数最多的数字。

          【讨论】:

            【解决方案8】:

            一种方法不是最精确的方法,但可以根据您的需要被认为“足够好”。

            该算法将在最小值和滑动最大值之间选择一个数字。将有一个保证的最大值g_max 和一个潜在的最大值p_max。您的真实最大值会根据另一个随机调用的结果而滑动。这将为您提供您正在寻找的倾斜分布。下面是 Python 中的解决方案。

            import random
            
            def get_roll(min, g_max, p_max)
            
                max = g_max + (random.random() * (p_max - g_max))
            
                return random.randint(min, int(max))
            
            get_roll(1, 10, 20)
            

            下面是使用 (1, 10, 20) 运行 100,000 次的函数的直方图。

            【讨论】:

              【解决方案9】:

              生成具有给定分布的随机数的一种简单方法是从列表中选择一个随机数,其中应该更频繁地出现的数字根据所需的分布重复。

              例如,如果您创建一个列表[1,1,1,2,2,2,3,3,3,4] 并从09 中选择一个随机索引以从该列表中选择一个元素,您将得到一个数字&lt;4 的概率为90%。

              或者,使用上面示例中的分布,生成一个数组[2,5,8,9] 并从09 中选择一个随机整数,如果它是≤2(这将以30% 的概率发生)然后返回1 ,如果是&gt;2≤5(这也有30%的概率会发生)返回2

              在此解释:https://softwareengineering.stackexchange.com/a/150618

              【讨论】:

              • 如果您希望结果 4 仅是每 100 万个,这将导致非常大的数组。还是?
              • @chrs 是的,这不是一个实用的方法,它只适用于小案例
              【解决方案10】:

              有很多方法可以做到这一点,所有这些基本上都归结为从右-skewed(也称为正偏态)分布生成。您没有明确说明您需要整数还是浮点结果,但有符合要求的离散分布和连续分布。

              最简单的选择之一是离散或连续右-triangular distribution,但是虽然这会让你逐渐减少你对更大值的渴望,但它不会让你独立控制平均值。

              另一种选择是截断指数(连续)或几何(离散)分布。您需要截断,因为原始指数或几何分布的范围从零到无穷大,因此您必须剪掉上尾。这反过来需要你做一些微积分来找到一个速率 λ,它在截断后产生所需的平均值。

              第三种选择是使用混合分布,例如在较低范围内以某个概率 p 均匀地选择一个数字,在较高范围内以概率 (1-p) 均匀地选择一个数字。然后,整体平均值为 p 乘以下限平均值 + (1-p) 乘以上限平均值,您可以通过调整范围和 p 的值来调入所需的总体平均值。如果您对子范围使用非均匀分布选择,此方法也将起作用。这一切都归结为您愿意投入多少工作来推导适当的参数选择。

              【讨论】:

                【解决方案11】:

                使用掉落表可以非常快速地滚动,这在实时游戏中很重要。实际上,它只是从一个范围内随机生成一个数字,然后根据概率表(该范围的高斯分布)一个带有多项选择的 if 语句。类似的东西:

                num = random.randint(1,100)
                if num<10 :
                    case 1
                if num<20 and num>10 :
                    case 2
                ...
                

                它不是很干净,但是当您有有限数量的选择时,它会非常快。

                【讨论】:

                  【解决方案12】:

                  您可以保留到目前为止从函数返回的内容的运行平均值,并基于此在 while 循环中获取满足平均值的下一个随机数,调整运行平均值并返回数字

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2015-06-19
                    • 2010-11-06
                    相关资源
                    最近更新 更多