生成RPG游戏中的随机数

6
我想知道是否有一种算法可以生成随机数,这些数字大部分时候会在一个从最小值到最大值的范围内较低。例如,如果您使用f(min: 1, max: 100, avg: 30)调用函数,在1到100之间生成随机数时,它应该大部分时间低于30,但如果您使用f(min: 1, max: 200, avg: 10)调用它,则平均值应该是10。很多游戏都这样做,但我无法找到一种使用公式实现此功能的方法。我看到的大多数示例使用“掉落表”或类似的东西。
我已经想出了一种相当简单的方法来加权投掷结果,但效率不高,并且您对其没有太多控制。
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次,每次迭代都会使用上次的结果更新max。因此,如果您使用N = 2,max = 100调用它,则必须连续两次掷出100才能返回100。
我查看了维基百科上一些分布,但我不太理解它们如何控制min和max等输出。
欢迎任何帮助。

想要的中位数是什么? - Dinesh
12个回答

8
生成符合特定分布的随机数的简单方法是从列表中选择一个随机数,其中应更频繁发生的数字按所需分布重复。例如,如果创建一个列表[1,1,1,2,2,2,3,3,3,4],并选择一个0到9的随机索引来从该列表中选择一个元素,则将以90%的概率获得小于4的数字。或者,使用上面示例中的分布,生成数组[2,5,8,9],并从0到9中选择一个随机整数,如果≤2(这将以30%的概率发生),则返回1,如果> 2且≤5(这也将以30%的概率发生),则返回2,等等。详见:https://softwareengineering.stackexchange.com/a/150618

如果您希望结果4仅为每100万个元素一次,那么这将导致非常大的数组。或者呢? - chrs
@chrs 是的,这不是一个实用的方法,它只适用于小规模的情况。 - user2314737

4
一个概率分布函数只是一个函数,当您输入一个值X时,它将返回获得该值X的概率。累积分布函数是获得小于或等于X的数字的概率。 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)。如果要使用有界公式,可以使用贝塔分布或库马拉斯瓦米分布。
  2. 对 PDF 进行积分得到累积分布函数。
  3. 找到 CDF 的反函数。
  4. 生成随机数并将其插入反函数中。
  5. 将结果乘以(max-min),然后加上 min。
  6. 将结果四舍五入到最近的整数。

步骤1到3是您必须在游戏中硬编码的内容。任何PDF文件的唯一解决方法是解出对应于其平均值的形状参数,并满足您希望形状参数具有的约束条件。如果您想使用Kumaraswamy分布,您将设置它,使得形状参数a和b始终大于一。

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

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

例如,如果我想要一个从0到100、a=2、b=5的概率分布如此形状的分布:

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被限制,因此您只需输入介于零和一之间的浮点数,然后将结果乘以(max-min)并加上min。Beta分布根据传递给它的值而有不同的边界。对于像PDF(x)= x这样的东西,CDF(x)=(x ^ 2)/ 2,因此您可以从CDF(0)到CDF(max-min)生成随机值。

缺点

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

这正是我正在寻找的。谢谢。 - chrs

1
你可以将两个随机过程组合在一起,例如: 第一个随机数 R1 = f(min: 1, max: 20, avg: 10); 第二个随机数 R2 = f(min: 1, max : 10, avg : 1); 然后将它们相乘得到的结果在[1-200]之间,并平均值大约为10(平均值会稍微偏移)。
另一种选择是找到想要使用的随机函数的反函数。这个选项必须在程序启动时初始化,但不需要重新计算。这里使用的数学方法可以在许多数学库中找到。我将通过以只有四个点已知的未知随机函数为例逐点解释。
  1. 首先,用三次或更高次多项式函数拟合四点曲线。
  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)。如果集成函数的反函数没有封闭形式的解(例如任何五次或更高次的一般多项式),则可以使用诸如牛顿法之类的根查找方法。
这种计算可以用来创建任何类型的随机分布。
编辑:
您可以在wikipedia中找到上述反变换抽样的描述,我发现了这个实现(我还没有尝试过)。

1

我会使用一个简单的数学函数。从你所描述的情况来看,你需要一种指数增长,比如y = x^2。在平均值处(平均值为x=0.5,因为rand可以得到0到1之间的数字),你将得到0.25。如果你想要一个更低的平均数,你可以使用更高的指数,比如y = x^3,这将导致在x = 0.5时y = 0.125。

示例: 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));

0

你可以保持一个运行平均值,根据迄今为止从函数返回的值,在 while 循环中获取下一个满足平均值的随机数,调整运行平均值并返回该数字。


0

有很多方法可以实现,基本上都是从一个右偏(也称为正偏)分布中生成。您没有明确说明您想要整数还是浮点数结果,但是有离散和连续的分布都适合。

其中最简单的选择之一是离散或连续的右三角形分布,但是虽然它会为较大的值提供所需的逐渐减少,但它不会给您独立控制平均值的能力。

另一个选择是截断指数(用于连续型)或几何(用于离散型)分布。您需要进行截断,因为原始指数或几何分布的范围从零到无穷大,因此您必须削减上限尾部。这反过来又需要您进行一些微积分,以找到在截断后产生所需平均值的速率λ。

第三种选择是使用分布的混合物,例如以概率p在较低范围内均匀选择一个数字,在较高范围内以概率(1-p)选择一个数字。总平均值就是p乘以较低范围的平均数加上(1-p)乘以较高范围的平均数,您可以通过调整范围和p的值来拨动所需的总平均数。如果在子范围中使用非均匀分布选项,这种方法也将起作用。这归结于您愿意为导出适当的参数选择付出多少工作量。

0
使用一个掉落表可以实现非常快速的掷骰,这在实时游戏中非常重要。实际上,它只是从一个范围内生成一个随机数,然后根据概率表(该范围的高斯分布)进行多项选择的if语句。就像这样:
num = random.randint(1,100)
if num<10 :
    case 1
if num<20 and num>10 :
    case 2
...

虽然不是很干净,但当你有有限的选择时,它可以非常快速。


0

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

算法是在最小值和滑动最大值之间选择一个数字。有一个保证最大值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次的直方图。

enter image description here


0
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
  • 如果高于30,则重新掷三次骰子,并根据新的点数设置掷出的点数;如果低于但大于等于30,则重新掷三次骰子,并根据新的点数设置掷出的点数
  • 如果低于30,则重新掷三次骰子,并根据新的点数设置掷出的点数;如果高于但小于等于30,则重新掷三次骰子,并根据新的点数设置掷出的点数
  • 如果恰好为30,则不重新设置掷出的点数
  • 返回掷出的点数

优点:

  • 简单易懂
  • 有效
  • 表现良好

缺点:

  • 由于30-70的关系,你自然会得到更多在30-40范围内的结果,而不是在20-30范围内的结果。

测试:

您可以使用以下方法与roll()方法一起进行测试。数据保存在哈希映射中(将数字映射到出现次数的数字)。

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是(如预期)出现次数最多的数字。

  • <10:4994
  • <20:9425
  • <30:18184
  • <40:29640
  • <50:18283
  • <60:10426
  • <70:5396
  • <80:2532
  • <90:897
  • <100:223

0
尝试这个方法,为平均数以下的数字范围生成一个随机数,并为平均数以上的数字范围生成第二个随机数。
然后随机选择其中一个,每个范围将有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”的数字的频率,反之亦然。 - Luke

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