需要可预测的随机发生器

时间:2009-05-26 11:31:40

标签: c++ algorithm random

我是网络游戏开发者,我遇到了随机数问题。假设玩家有20%的几率用他的剑获得重击。这意味着,5次点击中的1次应该是至关重要的。问题是我的现实生活成绩非常糟糕 - 有时候球员在5次安打中得到3次降落,有时15次命中都没有。战斗相当短(3-10次命中),因此获得良好的随机分布非常重要。

目前我使用PHP mt_rand(),但我们只是将代码转移到C ++,所以我想在游戏的新引擎中解决这个问题。

我不知道解决方案是否是一个统一的随机生成器,或者可能记住以前的随机状态以强制正确分发。

37 个答案:

答案 0 :(得分:223)

  

这意味着,5次点击中的1次应该是至关重要的。问题是我的现实生活成绩非常糟糕 - 有时候球员在5次安打中得到3次降低,有时15次命中都没有。

您需要的是shuffle bag。它解决了游戏中真随机过于随机的问题。

算法大致如下:你将1个关键命中和4个非关键命中放入一个包中。然后你将他们的订单随机化,并一次挑出一个。当行李是空的时,再次使用相同的值填充它并随机化。通过这种方式,您每5次点击平均可获得1次重击,最多连续2次关键命中和8次非关键命中。增加包中的物品数量,以增加随机性。

以下是我前段时间写过的an implementation(在Java中)和its test cases的示例。

答案 1 :(得分:113)

你对随机意味着误解了。

其中哪一个更随机?

enter image description here enter image description here

虽然第二个图看起来分布更均匀,但随机实际上是第一个图。人类的思维经常看到随机性的模式,所以我们将第一个图中的团块视为模式,但它们不是 - 它们只是随机选择的样本的一部分。

答案 2 :(得分:88)

考虑到你要求的行为,我认为你正在随机化错误的变量。

不是随机化命中是否至关重要,而是尝试随机化转数,直到发生下一次重击。例如,只需选择介于2和2之间的数字。每当玩家获得一个关键时,然后在经过多轮之后给他们下一个关键时刻。您还可以使用骰子方法来接近正态分布 - 例如,您将在2D4轮次中获得下一个关键。

我相信这种技术也可用于在世界范围内随机遭遇的RPG中 - 你随机化一个步数计数器,然后经过很多步骤,你再次被击中。感觉更加公平,因为你几乎不会连续遭遇两次遭遇 - 如果这种情况发生一次,那么球员就会变得烦躁。

答案 3 :(得分:53)

首先,定义“正确”的分配。随机数是随机的 - 您看到的结果与(伪)随机性完全一致。

扩展这一点,我假设你想要的是一种“公平”的感觉,所以用户不能没有成功就转100圈。如果是这样,我将跟踪自上次成功以来的失败次数,并对生成的结果进行加权。让我们假设你想要五分之一的卷“成功”。所以你随机生成一个从1到5的数字,如果它是5,那就很好。

如果没有,请记录失败,然后下一次生成1到5之间的数字,但加上说,floor(numFailures / 2)。所以这一次,他们又有五分之一的机会。如果失败,下次获胜间隔为4 5;成功的机会是五分之二。有了这些选择,经过8次失败后,他们肯定会成功。

答案 4 :(得分:39)

我同意之前的答案,即在一些游戏的小规模运行中真正的随机性是不可取的 - 对某些用例来说似乎太不公平了。

我在Ruby中编写了一个简单的Shuffle Bag,并进行了一些测试。实施这样做:

  • 如果它看起来仍然公平,或者我们没有达到最低滚动的门槛,则会根据正常概率返回一个公平的命中。
  • 如果从过去的卷观察到的概率看起来不公平,则会返回“公平的”点击。

基于边界概率,这被认为是不公平的。例如,对于20%的概率,您可以将10%设置为下限,将40%设置为上限。

使用这些边界,我发现在10次点击的情况下, 14.2%的时间真正的伪随机实现产生的结果超出了这些界限。大约11%的时间,0次关键命中在10次尝试中得分。 3.3%的时间,5个或更多的关键命中率从10个中降落。当然,使用这种算法(最小滚数为5),“Fairish”运行的数量少得多(0.03%)超出界限。即使下面的实现不合适(当然可以做更聪明的事情),值得注意的是,通常你的用户会觉得它对真正的伪随机解决方案是不公平的。

这是我用Ruby编写的FairishBag的内容。整个实施和快速蒙特卡罗模拟is available here (gist)

def fire!
  hit = if @rolls >= @min_rolls && observed_probability > @unfair_high
    false
  elsif @rolls >= @min_rolls && observed_probability < @unfair_low
    true
  else
    rand <= @probability
  end
  @hits += 1 if hit
  @rolls += 1
  return hit
end

def observed_probability
  @hits.to_f / @rolls
end

更新:使用此方法确实可以将获得重击的总体概率提高到使用上述范围的约22%。您可以通过将其“实际”概率设置得稍低来抵消这一点。修改后的概率为17.5%,观察到的长期概率约为20%,并使短期运行保持公平。

答案 5 :(得分:35)

用这样的东西替换mt_rand()怎么样?

XKCD comic (RFC 1149.5 specifies 4 as the standard IEEE-vetted random number.)

(RFC 1149.5将4指定为标准的IEEE审查随机数。)

来自XKCD

答案 6 :(得分:34)

希望本文能为您提供帮助: http://web.archive.org/web/20090103063439/http://www.gamedev.net:80/reference/design/features/randomness/

这种生成“随机数”的方法在rpg / mmorpg游戏中很常见。

它解决的问题是这个(摘录):

  

刀片蜘蛛在你的喉咙。它打了你想念。它再次击中,你再次错过。一次又一次,直到你没有任何东西可以击中。你已经死了,还有一个两吨重的蜘蛛在你的尸体上幸灾乐祸。不可能?不可以吗?是。但是如果给予足够多的球员并给予足够的时间,那么不可能的几乎可以肯定。这不是刀片蜘蛛很难,这只是运气不好。多么令人沮丧。这足以让玩家想要退出。

答案 7 :(得分:19)

你想要的不是随机数,而是对人类来说似乎随机的数字。其他人已经提出了个人算法,可以帮助你,比如Shuffle Bad。

有关此域名的详细而详尽的分析,请参阅AI Game Programming Wisdom 2。整本书值得为任何游戏开发者阅读,“貌似随机数”的概念在章节中处理:

人工智能决策和游戏逻辑的过滤随机性

摘要:传统观点认为,随机数生成器越好,您的游戏就越难以预测。然而,根据心理学研究,短期内的真正随机性往往对人类来说显然是不随意的。本文展示了如何使随机AI决策和游戏逻辑看起来更随机,同时仍保持强大的统计随机性。

您可能还会发现另一个有趣的章节:

随机数统计

摘要:随机数字最常用于人工智能和游戏。忽视他们的潜力是让游戏变得可预测和无聊。错误地使用它们可能就像完全忽略它们一样糟糕。了解随机数如何生成,它们的局限性和能力,可以消除在游戏中使用它们的许多困难。本文提供了对随机数,它们的生成以及将好的和坏的方法分开的方法的见解。

答案 8 :(得分:8)

您最好的解决方案可能是使用多种不同的随机方案进行游戏测试,并选择让玩家最开心的方案。

您也可以尝试在给定遭遇中针对相同号码的退避政策,例如,如果玩家在第一个回合中滚动1接受它。要获得另一个1,他们需要连续滚动2 1。要获得第三个1,他们需要连续3个,无限期。

答案 9 :(得分:8)

当然,任何随机数生成都有可能产生这样的运行?您不会在3-10卷中获得足够大的样本集以查看适当的百分比。

也许你想要的是一个怜悯的门槛......记住最后10卷,如果他们没有受到重击,给他们一个免费赠品。平滑随机的吊索和箭头。

答案 10 :(得分:7)

mt_rand()基于Mersenne Twister实现,这意味着它可以获得您可以获得的最佳随机分布之一。

显然你想要的不是随机性,所以你应该开始准确指出你想要的东西。你可能会意识到你有相互矛盾的期望 - 结果应该是真正随机的而且不可预测的,但同时它们不应该表现出来自所述概率的局部变化 - 但随后它变得可预测。如果你连续设置了最多10个非crits,那么你刚刚告诉玩家“如果你连续有9个非crit,那么下一个将是100%确定的关键” - 你可能会好吧,根本没有随意性。

答案 11 :(得分:7)

不幸的是,您要求的实际上是一个非随机数生成器 - 因为您希望在确定下一个数字时考虑以前的结果。这不是随机数生成器的工作方式,我担心。

如果您希望每5次点击中有1次是关键,那么只需选择一个介于1和5之间的数字,并说该命中将是至关重要的。

答案 12 :(得分:6)

在这么少的测试中你应该得到如下结果:

真正的随机性只能在巨大的设定尺寸上预测,因此很有可能第一次翻转硬币并连续3次获得头部,但是在几百万次翻转时,你最终将大约50-50。

答案 13 :(得分:6)

我看到很多答案建议跟踪以前生成的数字或者将所有可能的值洗牌。

就我个人而言,我不同意,连续3次爆炸是坏事。我也不同意连续15次非诅咒是坏事。

我会通过在每个数字之后修改它自己的暴击几率来解决问题。 示例(以演示该想法):

int base_chance = 20;
int current_chance = base_chance;

int hit = generate_random_number(0, 100) + 1; // anything from 1 to 100
if(hit < current_chance)//Or whatever method you use to check
{
    //crit!
    if(current_chance > base_chance)
        current_chance = base_chance; // reset the chance.
    else
        current_chance *= 0.8; // decrease the crit chance for the NEXT hit.
}
else
{
    //no crit.
    if(current_chance < base_chance)
        current_chance = base_chance; // reset the chance.
    else
        current_chance *= 1.1; // increase the crit chance for the NEXT hit.
    //raise the current_chance
}

你没有得到暴击的时间越长 - 你下一次暴击行为的可能性就越大。我包含的重置是完全可选的,它需要测试来判断它是否需要。在长期非暴击行动链之后,连续多次行动的暴击概率可能更高或可能不理想。

只要投入我的2美分......

答案 14 :(得分:5)

如果您想要一个不鼓励重复值的分布,您可以使用简单的重复拒绝算法。

e.g。

int GetRand(int nSize)
{
    return 1 + (::rand() % nSize);
}
int GetDice()
{
    static int nPrevious=-1;
    while (1) {
        int nValue = GetRand(6);
        // only allow repeat 5% of the time
        if (nValue==nPrevious && GetRand(100)<95)
            continue;
        nPrevious = nValue;
        return nValue;
    }
}

此代码在95%的时间内拒绝重复值,重复不太可能但不是不可能。 从统计上来说它有点难看,但它可能会产生你想要的结果。当然,它不会阻止像“5 4 5 4 5”这样的分发。你可以得到更好的并且拒绝倒数第二(比如说)60%的时间和第三名(比如说)30%。

我不推荐这个好游戏设计。简单地建议如何实现你想要的。

答案 15 :(得分:5)

前几个答案都是很好的解释,所以我只关注一个算法,让你可以控制“坏条纹”的概率,而永远不会成为确定性的。以下是我认为你应该做的事情:

而不是指定 p ,伯努利分布的参数,即你的爆击概率,指定 a b , β分布的参数,伯努利分布的“共轭先验”。您需要跟踪 A B ,即目前为止的关键和非关键命中数。

现在,要指定 a b ,请确保/(a + b)= p,即遭遇致命一击的可能性。巧妙的是,(a + b)量化了你希望A /(A + B)与p一般接近的程度。

你做这样的采样:

p(x)成为beta分布的概率密度函数。它可以在很多地方使用,但您可以在GSL中找到它作为gsl_ran_beta_pdf。

S = A+B+1
p_1 = p((A+1)/S)
p_2 = p(A/S)

通过采样概率为p_1 /(p_1 + p_2)的bernoulli分布选择一个致命一击

如果您发现随机数有太多“坏条纹”,请向上扩展 a b ,但在限制范围内,如 a b 进入无限,你将采用之前描述的随机包方式。

如果您执行此操作,请告诉我它是怎么回事!

答案 16 :(得分:4)

你想要的并不是很清楚。可以创建一个函数,使得前5次调用它,它会以随机顺序返回数字1-5。

但这并不是随机的。玩家将知道他将在接下来的5次攻击中获得5分。它可能是你想要的,在这种情况下,你只需要自己编写代码。 (创建一个包含数字的数组,然后将它们混洗)

或者,您可以继续使用当前的方法,并假设您当前的结果是由于一个糟糕的随机生成器。请注意,您当前的数字没有任何问题。随机值是随机的。有时你连续得到2,3或8个相同的值。因为它们是随机的。一个好的随机生成器只保证平均而言,所有数字都会经常返回。

当然,如果你一直在使用糟糕的随机生成器,那可能会使你的结果出现偏差,如果是这样,只需切换到更好的随机生成器就可以解决问题。 (查看Boost.Random库以获得更好的生成器)

或者,您可以记住随机函数返回的最后N个值,并按结果权衡结果。 (一个简单的例子是,“对于新结果的每次出现,有50%的可能我们应该丢弃该值并获得一个新值”

如果我不得不猜测,我会说坚持“实际”随机性是你最好的选择。确保使用一个好的随机生成器,然后继续按照现在的方式进行。

答案 17 :(得分:4)

您可以创建一个包含1到5之间数字的列表,并按随机顺序排序。然后只需浏览您创建的列表。你可以保证每次至少运行一次...当你完成前5个时,只需创建另外5个数字......

答案 18 :(得分:4)

我建议像暴雪一样使用渐进式百分比系统: http://www.shacknews.com/onearticle.x/57886

通常你会掷RNG,然后将它与一个值进行比较,以确定是否成功。这可能看起来像:

if ( randNumber <= .2 ) {
   //Critical
} else {
   //Normal
}

您需要做的就是逐渐增加基本机会......

if (randNumber <= .2 + progressiveChance ) {
   progressiveChance = 0;
   //Critical
} else {
   progressiveChance += CHANCE_MODIFIER;
   //Normal hit
}

如果你需要它更加花哨,那么添加更多就很容易。您可以限制progressiveChance可以获得的数量,以避免100%的关键机会或在某些事件上重置它。你也可以通过渐进式增强+ =(1 - progressiveChance)* SCALE,其中SCALE&lt; 1。

答案 19 :(得分:4)

好吧,如果你稍微学习数学,你可以试试Exponential distribution

例如,如果lambda = 0.5,期望值是2(去看那篇文章!),意味着你很可能会在第二回合命中/暴击/任何东西(比如50%,是吧?)。但是如果有这样的概率分布,你将在第0回合(那个已经发生并且已经重置了turn_counter的那个)定义错过(或做任何相反的事情),有40%的机会下一回合,大约65%有机会第二次(下一次接下来)转弯,约80%达到第三等等。

分配的全部目的是,如果一个人有50%的命中率并且他连续三次失误,那么他的确很可能(好的,有超过80%的几率,并且每下一回合都会增加)。它导致更“公平”的结果,保持50%的机会不变。

你有20%的暴击几率,你有

  • 17%暴击第一回合
  • 如果之前的所有比赛都没有发生任何爆击,那么第二回合比例为32%。
  • 如果之前的所有比赛都没有发生任何爆击,那么第三回合的爆击率为45%。
  • 如果之前的所有曲目都没有发生任何爆击,那么第四回合就会达到54%。
  • ...
  • 如果之前的所有比赛都没有发生任何爆击,那么80%即可获得第8回合。

在5次随后的转弯中,其仍有约0.2%(相当于5%)3次摔倒+ 2次非摔伤的几率。 并且有4%的可能性导致4次非击溃,5%的5次,1.5%的6次,0.3%的7次,0.07%的8次随后的非击球。我打赌它“比较公平”,而不是41%,32%,26%,21%和16%。

希望你仍然不会厌倦死亡。

答案 20 :(得分:3)

如何让关键的机会取决于最近的N次攻击。一个简单的方案是某种马尔可夫链:http://en.wikipedia.org/wiki/Markov_chain但是代码非常简单。


IF turns_since_last_critical < M THEN 
   critial = false
   turns_since_last_critical++;
ELSE
   critial = IsCritical(chance);
   IF Critial THEN
       turns_since_last_critica = 0;
   ELSE
       turns_since_last_critica++;
   END IF;
END IF;

当然,你必须做出自己的数学,因为一旦你知道自从上一次以来它已经足够了,一个关键的可能性低于一个关键的几率

答案 21 :(得分:2)

当您可能需要正态分布时,您正在查看线性分布。

如果你还记得在你年轻时玩D&amp; D时,你被要求滚动多个n面模具,然后对结果求和。

例如,轧制4 x 6面模具与轧制1 x 24面模具不同。

答案 22 :(得分:2)

alt text

这个是真的可以预测......但你永远无法确定。

答案 23 :(得分:2)

英雄之城实际上有一个叫做“破线者”的机械师,可以解决这个问题。它的工作方式是,在与字符串中最低命中概率相关的长度的一串未命中之后,保证下一次攻击是命中。例如,如果你错过了超过90%的攻击机会,那么你的下一次攻击会自动命中,但是如果你的命中几率低于60%,那么你需要连续几次失误来触发“连胜”(I)不知道确切的数字)

答案 24 :(得分:2)

OP,

相当多,如果你想要它是公平的,它不会是随机的。

你的游戏问题是实际的比赛长度。匹配时间越长,您将看到的随机性越小(crits将趋于20%)并且它将接近您的预期值。

您有两种选择,根据之前的滚动预先计算攻击。每5次攻击你会获得一次暴击(基于你的20%),但你可以随机发出命令。

listOfFollowingAttacks = {Hit,Hit,Hit,Miss,Crit};

这就是你想要的模式。所以让它从该列表中随机选择,直到它为空,然后重新创建它。

这是我为我的游戏创建的一种模式,它可以很好地运行,我想要它做什么。

你的第二个选择是,增加暴击的机会,你可能会在所有攻击结束时看到一个更均匀的数字(假设你的比赛结束得相当快)。机会越少,你获得的RNG就越多。

答案 25 :(得分:0)

我建议以下“随机延迟回扣模具”:

  • 维护两个数组,一个(in-array)最初填充0到n-1之间的值,另一个(out-array)为空
  • 请求结果时:
    • in-array
    • 中的所有定义的值返回一个随机值
    • 将此值从in-array移至out-array
    • out-array中的一个随机(通过所有元素,包括未定义!)元素移回in-array

这具有以下特性:它会比较大的 n 更慢地“反应”。例如,如果你想要20%的几率,将 n 设置为5并且点击0比“ n 设置为10并且点击0或者0更少随机”在小样本中,将其从0到199中的1000与几乎无法区分真正的随机性。您必须将 n 调整为您的样本量。

答案 26 :(得分:0)

我想也许您使用的是错误的随机分布函数。 您可能不希望均匀分布数字。尝试正常分布,以使关键命中比“常规”命中变得更加罕见。

我使用Java,因此我不确定在哪里可以找到适合C ++的东西,它可以为您提供正态分布的随机数,但必须有一些东西。

答案 27 :(得分:0)

正如许多人所说,这是“随机”的问题。你获得的结果是随机的,无论你如何制作游戏,一些玩家都会觉得你的反击是不公平的,也不是随意的。 ;)

一种可能的选择可能是每n次保证一次命中,并且在每次命中后在某些边界内随机生成n。这完全是游戏测试中“感觉”正确的问题。

答案 28 :(得分:0)

反应: “问题是我的现实生活成绩非常糟糕 - 有时候球员会在5次安打中获得3次降落,有时15次安打都没有。”

你有可能在15次点击中获得任何东西的3%到4%......

答案 29 :(得分:0)

预先计算每个玩家的随机重击

// OBJECT
//...
// OnAttack()
//...
c_h = c_h -1;
if ( c_h == 0 ) {
 // Yes, critical hit!
 c_h = random(5) + 1 // for the next time
 // ...
}

答案 30 :(得分:0)

它应该是怎样的,这是概率,你不应该期望每5次战斗的重击1次。用硬币或骰子尝试并自己看看。但那只是我。 GB

答案 31 :(得分:0)

我也尝试过解决这个问题。我能想出的最好的结果是,如果这些数字基于他们在过去的频率出现的频率,就会动态地改变概率。类似的东西(对于骰子,在matlab中):

probabilities = [1 1 1 1 1 1];
unrandomness = 1;
while true
    cumprob = cumsum(probabilities) ./ sum(probabilities);
    roll = find(cumprob >= rand, 1)
    probabilities = probabilities + unrandomness;
    probabilities(roll) = probabilities(roll) - 6*unrandomness;
    if min(probabilities) < 0
        probabilities = probabilities - min(probabilities);
    end
end

抱歉没有缩进。可以根据需要调整非随机性参数。真随机输出(非随机性= 0):

2 3 1 1 4 1 3 3 4 5 1 5 5 2 6 1 1 1 6 1 1 3 5 6 6 1 4 2 4 6 3 6 5 1 1 6 2 5 6 4 3 5 2 4 6 5 5 5 4 4 3 4 1 2

unrandomness = 1:

3 2 4 5 6 2 6 2 4 1 5 5 1 6 4 3 1 4 2 1 3 2 6 5 3 6 5 3 1 4 1 6 5 3 4 2 1 6 5 4 1 4 3 1 6 6 5 4 3 1 5 2 3 2

我认为看起来更好。特别是如果你绘制数字之间的差距。

答案 32 :(得分:0)

我认为您需要从泊松分布中生成随机数。

答案 33 :(得分:0)

如何对值进行加权?

例如,如果您有20%的几率发生重击,请生成一个1到5之间的数字,其中一个数字代表一个重要命中,或者一个数字介于1到100之间,其中20个数字是一个重要命中。

但只要您使用随机数或伪随机数,就无法避免您目前看到的结果。这是随机性的本质。

答案 34 :(得分:-1)

bozo sort的问题,它有可能永远占用。

答案 35 :(得分:-2)

对于C ++随机数生成器,请使用randboost::random

每当玩家被击中时,你只需检查[0; 1]小于0.2。 这意味着某人在15岁时不会受到重击,但这是不可能的。

这将根据binomial distribution(p = 0.2)

的定律给出自然随机数

答案 36 :(得分:-4)

static int crit = 0;

public bool isCritical()
{
   crit = crit++ % 5;
   return (crit==0);
} 

如果你仍然需要一些随机性,每次执行暴击时使用另一个静态变量来改变模数。改变它将等于3到7的概率,应该保持平均时间为1比5,但不要低于3或超过7次点击。