JavaScript中更好的随机函数

18

我正在使用 JavaScript 制作康威生命游戏的复制品,并发现函数 Math.random() 总是返回某种模式。这是一个在 100x100 网格中随机生成结果的示例:

enter image description here

有人知道如何获得更好的随机数吗?

ApplyRandom: function() {

    var $this = Evolution;

    var total = $this.Settings.grid_x * $this.Settings.grid_y;
    var range = parseInt(total * ($this.Settings.randomPercentage / 100));

    for(var i = 0; i < total; i++) {
      $this.Infos.grid[i] = false;
    }

    for(var i = 0; i < range; i++) {
      var random = Math.floor((Math.random() * total) + 1);
      $this.Infos.grid[random] = true;
    }

    $this.PrintGrid();
  },

[更新]

我在这里创建了一个jsFiddle:http://jsfiddle.net/5Xrs7/1/

[更新]

看来 Math.random() 最终还是没问题的(感谢 raina77ow)。对不起,大家! :(. 如果您对结果感兴趣,这里是游戏的更新版本:http://jsfiddle.net/sAKFQ/

(但我认为还有一些 bug...)


4
你能给我们展示一些代码吗,@Danny? - Dogbert
3
相关链接:https://dev59.com/GV3Va4cB1Zd3GeqPD81N - Factor Mystic
4
赞同@Dogbert,看起来这确实是一个XY问题。 Math.random()本身就非常随机。它的结果可能可以被预测(如本文所示),但它绝对不会像这里展示的那样遵循如此清晰的模式。还有其他问题存在。 - raina77ow
3
@Ariane:我想指出,将随机数相乘只会改变结果的概率分布;它们很可能会变得*不那么随机。考虑从 {1,2} 中进行选择的概率。您得到 2 的频率与获得 1 或 4 的组合结果的频率一样多。 - JayC
4
我知道这个问题现在已经解决了,结果证明Math.Random()并不是问题所在,但为了让其他有类似问题的人受益,值得重申一下这句箴言:每当你认为在使用的库中发现了一个错误时,很有可能实际上是你自己代码中的错误,而不是核心库中的错误。Jeff Atwood 的表述更好,我只需在此链接他的博客:编程的第一原则:任何问题都是你的问题 - Spudley
显示剩余10条评论
7个回答

12

你代码中的这一行...

var position = (y * 10) + x;

...是导致这种“非随机性”的原因。它真的应该是...

var position = (y * $this.Settings.grid_x) + x;

我认为这个网格最初的大小是10,所以它在这里。但显然是错误的:您应该根据当前网格的大小选择位置。


顺便说一下,没有冒犯之意,但我仍认为@JayC答案中给出的算法优于您的算法。而且很容易实现,只需将ApplyRandom函数中的两个循环改为单个循环:

var bias = $this.Settings.randomPercentage / 100;
for (var i = 0; i < total; i++) {
  $this.Infos.grid[i] = Math.random() < bias;
}

通过这个改变,你将不再遭受在var random = Math.floor((Math.random() * total) + 1);行中重复使用相同数字的副作用,这会降低你原始代码中实际单元格的填充率。


6
作为一个旁注,我简直无法相信这里有多少人认为在问题中展示的模式可能是由于Math.random()的非随机性造成的。 - raina77ow
不,这不是关于你的。)有时候当我在处理大型代码库并出现了错误时,我也会开始怀疑基础知识,所以我完全理解你的感受。 - raina77ow
2
y的乘数应该是grid_x,即网格线的宽度。目前它们相等,所以没有区别,但如果尺寸被变成非正方形,某些部分将会出错。 - Joni
此外,你在 ApplyRandom 结束时和 Init 中的代码中都调用了 PrintGrid。由于 PrintGrid 修改了网格,所以你显示的初始步骤实际上是第一次迭代后的状态。 - rsanchez
2
@DannyCoulombe 为了更好的程序结构,你应该将推进游戏的代码从PrintGrid移动到一个新的函数中。 - rsanchez

3

Math.random是一种伪随机方法,这就是为什么你会得到那些结果的原因。我经常使用的一个方法是捕捉鼠标指针位置,以便向Math.random结果添加一些随机值:

Math.random=(function(rand) {
  var salt=0;
  document.addEventListener('mousemove',function(event) {
    salt=event.pageX*event.pageY;
    });
return function() { return (rand()+(1/(1+salt)))%1; };
})(Math.random);

它并不完全是随机的,但比随机略微更多 ;)


2
更好的解决方案可能不是随机选择点并将它们涂成黑色,而是遍历每个点,决定它应该填充的概率,然后进行填充。也就是说,如果你想让平均填充20%的几率,请生成随机数r,当r < 0.2时进行填充。我曾经看到过一个WebGL生命模拟器,初始化时就是这样做的...如果我没记错的话。
编辑:还有一个考虑替代涂色方法的原因。随机选择像素可能会导致更少的工作量和更少的随机数生成,这取决于你想要什么,这可能是件好事。当前的做法最多只会填充一定百分比的像素。如果你跟踪已填充的像素,并选择在另一个像素已被填充时填充另一个像素,那么你所做的实际上只是在把黑色像素以严格百分比混合在白色像素中。按照我的方式做,所选像素的百分比将遵循二项分布。有时填充百分比会稍微多一些,有时则少一些。所有混合的集合是这种选择方式生成的可能性的严格子集(而这种选择方式严格来说包含了涂色板的所有可能性,只是获得大部分可能性的概率极低)。简单地说,每个像素随机选择将允许更多的差异。
不过,我可以修改洗牌算法,根据期望/平均值生成从二项概率分布函数中选择像素百分比的数字,而不是期望/平均值本身,而且我真的不知道这会有什么区别-至少在理论上是这样。有很多事情可以做。

是的,这是更好的解决方案。不过看看原始代码哪里出了问题也很有趣。) - raina77ow

1

0
你可以使用包括纳秒的时间戳中的sha256哈希部分:
console.log(window.performance.now()); //return nanoseconds inside

这可以被编码为字符串,然后你可以使用这个链接获取哈希值:http://geraintluff.github.io/sha256/

salt = parseInt(sha256(previous_salt_string).substring(0, 12), 16);
//48 bits number < 2^53-1

然后,使用@nfroidure的函数, 在之前编写gen_salt函数,使用sha256哈希, 并将gen_salt调用写入eventListener。 您可以使用sha256(previous_salt) + mouse coordinate作为字符串来获取随机哈希。

0
< p > 实现 < code > Math.random 可能基于 线性同余生成器 ,其中一个弱点是随机数依赖于先前的值,根据算法中常量的选择产生可预测的模式,例如 RANDU 这样的著名例子说明了选择不当的常量所造成的影响。

梅森旋转演算法 随机数生成器没有这个弱点。你可以在这里找到 JavaScript 的 MT 实现: https://gist.github.com/banksean/300494


更新:看到你的代码,你在渲染网格的代码中有一个问题。这一行:

var position = (y * 10) + x;

应该是:

var position = (y * grid_x) + x;

通过这个修复,没有明显的模式。


0

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