公开透明

转盘真的是随机的吗?

是的。每次转动在动画开始之前,就已经用浏览器自带的密码学随机数生成器决定了结果。服务器不参与、不干预;除非你自己设置了概率,否则每一格机会完全相同。

实际运行的代码
const MAX = 2 ** 32

function randomInt(n) {
  // 丢掉不能整除的尾部,让 n 个结果机会完全相等
  const limit = MAX - (MAX % n)
  let value
  do {
    value = crypto.getRandomValues(new Uint32Array(1))[0]
  } while (value >= limit)
  // 此时取模不再有偏差
  return value % n
}

这是本站共享随机模块里的 randomInt 函数。你可以在浏览器开发者工具的“源代码”面板中查看完整脚本。

亲自试试:模拟转动 10,000 次

8 个等大的格子。公平的转盘每格大约会中 1,250 次,并带有正常的小幅波动。

次数
格数
概率

虚线:完全公平的转盘在该格的理论期望次数

与期望值的最大偏差–
卡方值,自由度 7–低于 14.07 即在 5% 显著性水平下通过
结论–

均等模式每次转动都调用左侧展示的 randomInt 函数,全部在你的浏览器里完成。 卡方检验把每根柱子与期望值的差距加总起来。即使是公平的随机数,大约每 20 次也会有 1 次超过 5% 的临界值,所以偶尔一次“偏离”很正常;如果每次都不通过,才说明有问题。

  • 直播和微信群抽奖开奖前把这页发给观众,说明中奖者是怎么抽出来的,减少“内定”质疑。
  • 课堂随机点名学生知道老师也控制不了转盘,被点到时就不会觉得是针对自己。
  • 朋友聚会做决定吃什么、谁买单、谁洗碗,有人说转盘有猫腻时,把这页甩给他。
  • 检查自定义概率看懂为什么 30% 的格子长期约每 10 次中 3 次,而短期会上下波动。
使用方法

按下“转动”之后发生了什么

  1. 你按下转动

    页面向浏览器的 crypto.getRandomValues 索取新的 32 位随机数,它由操作系统的安全随机源提供种子。

  2. 停止位置当场确定

    这些随机数决定转盘多转几圈、最终停在哪个角度。此刻结果已经固定:停下时指针下面的那一格就是中奖项。

  3. 然后才播放动画

    旋转、减速和滴答声都只是视觉效果。看得再久、手机再快,也改变不了已经定好的停止角度。

  4. 数据不离开你的设备

    抽取在你的浏览器中完成,名单不会上传到我们的服务器来决定结果,所以不存在后台“开关”可以操控。

大家为什么用它

保证每次抽取公平的六个设计

密码学级随机数crypto.getRandomValues 是浏览器生成安全密钥用的随机源,而不是可预测的 Math.random。
没有取模偏差拒绝采样会丢掉少数会偏向小数字的原始值,让 n 个结果各自正好是 n 分之一。
Fisher–Yates 洗牌打乱名单、分组、排顺序都用 Fisher–Yates 算法,每一种排列出现的机会都相同。
动画前就已决定按下转动的瞬间停止角度就固定了,速度设置只影响你看多久。
服务器不参与结果在你的设备上计算,没有账号、没有隐藏队列,也没有远程干预。
只有你设置才加权自定义百分比会画成大小不同的格子,转盘上看到的面积就是真实概率。

为什么直接取模会有一点不公平,拒绝采样又怎么解决

随机数生成器给出的是固定范围内的整数。浏览器给的是 32 位整数,一共 4,294,967,296 种可能。最直接的做法是用 value % n,也就是除以 n 取余数,来从 n 个选项里挑一个。问题在于:除非 2 的 32 次方能被 n 整除,否则总有几个余数会多出现一次。

用一个只能生成 0 到 7 的迷你生成器、从 3 个选项里抽,最容易看清楚。

方法选项 1选项 2选项 3
直接取模,取值 0 到 70、3、6:8 分之 31、4、7:8 分之 32、5:只有 8 分之 2
拒绝采样,丢掉 6 和 7 重抽0、3:6 分之 21、4:6 分之 22、5:6 分之 2

本站的 randomInt 先算出 limit,即 2 的 32 次方减去余数,凡是大于或等于 limit 的值都丢掉重抽。32 位随机数下这点偏差本来就极小,一个班级人数规模的名单不到一亿分之一,重抽也几乎不会发生,但结果从“几乎准确”变成了“完全准确”。

转盘是怎么把随机数变成中奖结果的

转盘并不是先偷偷选好一个序号再假装转动,而是直接随机出一个物理停止角度:额外转 5 到 8 圈之间的随机圈数,再加一个随机的最终角度,两者都来自 crypto.getRandomValues。每一格占据圆周的一部分,所以它停在指针下的概率就正好等于它占 360 度的比例,误差约为四十亿分之一。

给选项设置自定义百分比后,转盘会按百分比重新画格子,而且所有百分比加起来必须等于 100 才能转。50% 的格子占半个圆,长期大约赢一半。不用转盘、直接从名单里抽的工具则使用 weightedIndex:在总权重上随机取一个点,依次往后找,看它落在哪一项。

  • 转动前转盘停在什么位置都不影响结果,因为最终角度不管起点在哪里都是均匀分布的。
  • 转动速度和时长只是拉长动画,停止角度在第一帧之前就定好了。
  • 移除中奖者后转盘会重新绘制,剩下的选项重新分配整个圆周。

连续抽中同一个人很正常:真随机长什么样

对随机转盘最常见的抱怨是“怎么又是他”。公平的随机没有记忆,所以重复不只是可能,而是意料之中。下一次重复上一次结果的概率,就是 1 除以选项数。

转盘上的选项数连续两次同一个结果转 20 次至少出现一次连中
250%几乎必然
425%约 99.6%
812.5%约 92%
128.3%约 81%
303.3%约 47.5%

如果你需要每个人都轮到一次再重复,那是“不放回抽取”,并不是更公平。在随机点名转盘上使用“移除并再转一次”,或者用列表随机排序工具一次性打乱整份名单。

哪里还用到了 Math.random,为什么不影响结果

Math.random 用来做装饰效果没有问题,所以少数动画会用到它:掷骰子时骰子的轻微倾斜、弹珠赛跑的晃动、数字定格前的滚动。这些情况下结果本身都已经用密码学随机数抽好了,装饰幅度也控制得足够小,不会改变你看到的顺序。

共享随机模块只有在浏览器完全不支持 Web Crypto 时才会退回 Math.random,这在今天只会出现在非常老旧的浏览器上。当前版本的 Chrome、Safari、Firefox、Edge 以及微信内置浏览器都支持 crypto.getRandomValues。

常见问题

常见问题解答

随机转盘真的是随机的吗?

本站的是。每次转动都用浏览器的密码学安全随机数 crypto.getRandomValues,在动画开始前决定转盘停在哪里。每一格中奖的概率与它的面积成正比,等大的格子机会完全相同。

网站或者转转盘的人能不能作弊、内定结果?

我们的服务器不决定、也不修改任何一次转动,因为抽取是在你的浏览器里完成的。转转盘的人也控制不了:在什么时机点击、按住多久,都不会影响随机出的停止角度。你可以在浏览器开发者工具中查看代码。

为什么同一个名字连续中了两次?

因为随机没有记忆。8 个选项时,同一项连续中两次的概率是 12.5%;连转 20 次,至少出现一次连中的概率约 92%。如果不想重复,每次抽中后把中奖者移除即可。

转得更久或更快,会改变概率吗?

不会。停止角度在你按下转动的瞬间就确定了,时长、速度和减速曲线只改变动画的样子,不改变停在哪里。

转盘的自定义概率是怎么算的?

给某个选项填上百分比,它的格子就会画成占圆周相应的比例,所有百分比需要加起来等于 100。设为 30% 的选项长期大约每 10 次中 3 次,但短期内会高于或低于这个数。

模拟器里的卡方值和结论是什么意思?

卡方值把每一格的实际次数与期望次数之差平方后、除以期望次数再加总。只要低于对应格数的 5% 临界值,这次结果就与公平随机相符。公平的生成器也大约每 20 次会有 1 次不通过,所以多跑几次再下结论。

抽奖结果是用 Math.random 生成的吗?

不是。Math.random 只用于骰子倾斜、弹珠晃动等视觉效果,而且是在结果已经抽出之后。结果来自 crypto.getRandomValues,只有在不支持 Web Crypto 的老旧浏览器中才会退回 Math.random。