转盘真的是随机的吗?
是的。每次转动在动画开始之前,就已经用浏览器自带的密码学随机数生成器决定了结果。服务器不参与、不干预;除非你自己设置了概率,否则每一格机会完全相同。
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 次,并带有正常的小幅波动。
虚线:完全公平的转盘在该格的理论期望次数
均等模式每次转动都调用左侧展示的 randomInt 函数,全部在你的浏览器里完成。 卡方检验把每根柱子与期望值的差距加总起来。即使是公平的随机数,大约每 20 次也会有 1 次超过 5% 的临界值,所以偶尔一次“偏离”很正常;如果每次都不通过,才说明有问题。
- 直播和微信群抽奖开奖前把这页发给观众,说明中奖者是怎么抽出来的,减少“内定”质疑。
- 课堂随机点名学生知道老师也控制不了转盘,被点到时就不会觉得是针对自己。
- 朋友聚会做决定吃什么、谁买单、谁洗碗,有人说转盘有猫腻时,把这页甩给他。
- 检查自定义概率看懂为什么 30% 的格子长期约每 10 次中 3 次,而短期会上下波动。
按下“转动”之后发生了什么
你按下转动
页面向浏览器的 crypto.getRandomValues 索取新的 32 位随机数,它由操作系统的安全随机源提供种子。
停止位置当场确定
这些随机数决定转盘多转几圈、最终停在哪个角度。此刻结果已经固定:停下时指针下面的那一格就是中奖项。
然后才播放动画
旋转、减速和滴答声都只是视觉效果。看得再久、手机再快,也改变不了已经定好的停止角度。
数据不离开你的设备
抽取在你的浏览器中完成,名单不会上传到我们的服务器来决定结果,所以不存在后台“开关”可以操控。
保证每次抽取公平的六个设计
为什么直接取模会有一点不公平,拒绝采样又怎么解决
随机数生成器给出的是固定范围内的整数。浏览器给的是 32 位整数,一共 4,294,967,296 种可能。最直接的做法是用 value % n,也就是除以 n 取余数,来从 n 个选项里挑一个。问题在于:除非 2 的 32 次方能被 n 整除,否则总有几个余数会多出现一次。
用一个只能生成 0 到 7 的迷你生成器、从 3 个选项里抽,最容易看清楚。
| 方法 | 选项 1 | 选项 2 | 选项 3 |
|---|---|---|---|
| 直接取模,取值 0 到 7 | 0、3、6:8 分之 3 | 1、4、7:8 分之 3 | 2、5:只有 8 分之 2 |
| 拒绝采样,丢掉 6 和 7 重抽 | 0、3:6 分之 2 | 1、4:6 分之 2 | 2、5:6 分之 2 |
本站的 randomInt 先算出 limit,即 2 的 32 次方减去余数,凡是大于或等于 limit 的值都丢掉重抽。32 位随机数下这点偏差本来就极小,一个班级人数规模的名单不到一亿分之一,重抽也几乎不会发生,但结果从“几乎准确”变成了“完全准确”。
转盘是怎么把随机数变成中奖结果的
转盘并不是先偷偷选好一个序号再假装转动,而是直接随机出一个物理停止角度:额外转 5 到 8 圈之间的随机圈数,再加一个随机的最终角度,两者都来自 crypto.getRandomValues。每一格占据圆周的一部分,所以它停在指针下的概率就正好等于它占 360 度的比例,误差约为四十亿分之一。
给选项设置自定义百分比后,转盘会按百分比重新画格子,而且所有百分比加起来必须等于 100 才能转。50% 的格子占半个圆,长期大约赢一半。不用转盘、直接从名单里抽的工具则使用 weightedIndex:在总权重上随机取一个点,依次往后找,看它落在哪一项。
- 转动前转盘停在什么位置都不影响结果,因为最终角度不管起点在哪里都是均匀分布的。
- 转动速度和时长只是拉长动画,停止角度在第一帧之前就定好了。
- 移除中奖者后转盘会重新绘制,剩下的选项重新分配整个圆周。
连续抽中同一个人很正常:真随机长什么样
对随机转盘最常见的抱怨是“怎么又是他”。公平的随机没有记忆,所以重复不只是可能,而是意料之中。下一次重复上一次结果的概率,就是 1 除以选项数。
| 转盘上的选项数 | 连续两次同一个结果 | 转 20 次至少出现一次连中 |
|---|---|---|
| 2 | 50% | 几乎必然 |
| 4 | 25% | 约 99.6% |
| 8 | 12.5% | 约 92% |
| 12 | 8.3% | 约 81% |
| 30 | 3.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。