跳过主要内容

抽取是怎么做的

不是「相信我们,它是随机的」。方法写在下面。

中奖者在转盘动起来之前就定了

每一次旋转都先抽出中奖者,再把动画对准它。正因为这个顺序,你甩得多用力都改变不了结果;也正因为它,开启「减少动态效果」得到的是同一次抽取、只是没有动画,而不是另一条代码路径。换句话说,你看到的旋转是对一个已经做出的决定的重演——这是上面两个承诺能同时成立的唯一方式。

随机性从哪来

来源是浏览器的密码学随机数生成器 crypto.getRandomValues,不是 Math.random。Math.random 在规范上允许可预测,在某些引擎上确实可预测;对一场抽奖来说这是有分别的。

为什么我们会扔掉一些随机数

把一个 32 位随机数用「取余数」变成「0 到 11 之间的数」,是一种不易察觉的不公平。2³² 除以 12 除不尽,于是排在前面的那几个选项每个都会多得到一次机会。在十二格的盘上这个偏差很小,但它是真的,它永远偏向同样那几格,而且不会随次数变多而抵消。所以我们不取余数,而是把落在那段除不尽的尾巴里的随机数丢掉、重新抽一个。代价是偶尔多抽一次,换来的是精确均匀。

const limit = Math.floor(2 ** 32 / max) * max;

let draw = crypto.getRandomValues(new Uint32Array(1))[0];
while (draw >= limit) {
  draw = crypto.getRandomValues(new Uint32Array(1))[0];
}

return draw % max;

权重,以及扇区为什么按权重画

加权抽取会把所有权重加起来,用同样的拒绝采样在总和之内均匀取一个数,再看这个数落在谁身上。而盘面会把每个扇区正好画成它在整圆里应占的比例。一个「看起来一样大、概率却不一样」的盘,正是我们所反对的那种欺骗——所以在我们这里,面积和概率不是靠规定一致,而是同一个数。

你的名单不会离开这台浏览器

没有账号,也没有服务端存储。名单存在这台浏览器里,分享链接则把整个盘面装在 URL 本身里。关于某一次抽取的任何信息,都不会被记录在我们能看到的地方。

你可以自己核对

抽取逻辑只有几十行,上面每一条主张都有一个测试盯着——只要它不再成立测试就会失败,其中一个会专门喂给生成器一串会触发模偏的数,然后断言结果仍然均匀。

一个盘最多装 1000 个条目,一个条目和 1000 个条目走的是同一套方法。