跳至主要內容
所有文章
運作原理

轉盤其實早已知曉答案

為什麼抽獎結果在動畫開始前就已抽出,以及這樣做帶來的好處。

按下「開始旋轉」,轉盤旋轉十秒鐘。看起來結果似乎是在這十秒的某個時刻決定的——在減速過程中、在最後的半圈裡,或在指標是否剛好越過下一道分割線。

其實並非如此。獲獎選項在幾微秒內就已率先抽出,隨後的旋轉動畫只是朝向該目標轉動。

為什麼採用這種順序

最顯而易見的替代做法是轉動輪盤,轉到哪裡就停在哪裡。這聽起來更公平——沒有人為預設,全憑物理規律——但實際上卻更難以證明其公正性,因為這會將所有關於公平性的質疑轉化為對動畫機制的質疑。

用力滑動會改變結果嗎?在旋轉中途點擊讓它提前停止會產生偏差嗎?對於開啟了 prefers-reduced-motion(減少動態效果)、完全看不到動畫的使用者又會發生什麼?

先抽後轉讓這三個問題有了同一個答案:不會,因為抽獎早已完成。用力滑動只會增加旋轉圈數;提前點擊只會縮短旋轉路徑;減少動態效果則直接跳過旋轉並立即顯示結果。同一套核心程式碼對應三種不同的呈現方式,不必分別為三條流程證明公平性。

抽獎的核心機制

隨機數來源是 crypto.getRandomValues——瀏覽器原生的密碼學安全隨機數生成器——絕非 Math.random

要從中獲得均勻分佈的數字,需要多做一步處理。直接使用 draw % count 進行取模運算,在 count 無法整除 2³² 時會產生偏差:排在前面的幾個選項在 40 億分之一的機率中會各自多出一次機會。偏差雖然極小,仍然是不平衡。因此,落在無法整除的餘數區間內的抽獎值會被直接捨棄並重新抽取。

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;

這樣做偶爾需要額外抽取一次,但能得到完全均勻的結果。相同的程式碼也展示在公平性頁面上,那就是實際執行的核心程式碼,不是另外寫的示範。

權重代表概率,而非偏袒

將某個選項的權重設為 3,它在抽獎中就會分得三份份額而非一份。轉盤會將其繪製成更寬的扇區,列表則會直接顯示該權重對應的實際百分比,無需手動計算。

兩個文字相同的選項仍會保留為兩個獨立選項。它們各自保留中獎概率,列表中會顯示發現的重複數量,而不是悄悄合併它們——在未告知的情況下將某人的中獎概率減半,是沒有人需要的多餘做法。

動畫的真正作用

預定目標的動畫並非虛假的動畫。轉盤確實會精準旋轉至中獎扇區,指針音效確實會與劃過的分割線精確同步,而「擦肩而過」的懸念效果——在開啟時——會停頓在中獎選項的相鄰扇區而非隨機扇區,隨後再轉向一開始就已抽出的中獎項目。

緊張的懸念感是真實的,只是結果比看起來更早確定而已。