メインコンテンツへスキップ
すべての記事
仕組み

ルーレットはすでに答えを知っている

なぜアニメーションが始まる前に当選者が抽選されるのか、そしてそれが何をもたらすのか。

「スピン」を押すと、ルーレットが10秒間回転します。その10秒間のどこか――減速の中、最後の半回転、あるいはポインターがもう1つの仕切りをギリギリ越えるかどうか――で結果が決まっているように見えます。

しかし、実際は違います。当選者は最初にわずか数マイクロ秒で抽選されており、アニメーションはその結果に合わせて狙い撃ちされているのです。

なぜその順序なのか

わかりやすいもう1つの方法は、ルーレットを回して止まった場所をそのまま読み取ることです。そのほうが公平に思えるかもしれません――誰も選んでおらず、物理法則が決めたのだから。ですが、それを証明・擁護するのはかえって困難です。なぜなら、公平性に関するすべての疑問がアニメーション自体の疑問にすり替わってしまうからです。

強くスワイプしたら結果を変えられるのか? 途中でクリックして早めに止めたら結果に偏りが出るのか? prefers-reduced-motion(視覚効果を減らす)を有効にしていてアニメーションを一切見ないユーザーの場合はどうなるのか?

最初に抽選を行えば、これら3つの疑問すべてに対する答えは同じ「いいえ」になります。抽選はすでに終わっているからです。強く弾けば回転数が増えるだけです。早めのクリックは移動距離を縮めるだけです。「視覚効果を減らす」設定なら移動を完全にスキップして即座に結果を表示します。個別に議論しなければならない3つの分岐を作るのではなく、1つのコードを通る3つの異なる演出表現にすぎないのです。

どのように抽選しているのか

エントロピー源には crypto.getRandomValues(ブラウザ標準の暗号学的乱数生成器)を使用しており、Math.random は決して使いません。

そこから均一な乱数を得るには、一見単純に見える以上の手順がもう1ステップ必要です。単純な draw % count(余剰演算)では、count が2³²を割り切れない場合に必ず偏り(剰余バイアス)が生じます。最初のいくつかの項目が、40億分の一の確率で1回分多く選ばれやすくなるのです。わずかな差ですが、天秤に指を乗せるようなものであり、公平を謳うルーレットにそのような偏りがあってはなりません。そのため、余りとなる末尾の範囲に落ちた抽選値は破棄され、再抽選が行われます。

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」に設定すると、抽選枠を1口ではなく3口分獲得します。ルーレット上ではより広い扇形として描画され、リストにはその比率が実際に何パーセントに相当するかが表示されるため、手作業で計算する必要はありません。

まったく同じテキストの項目が2つある場合、それらは2つの独立した項目のまま維持されます。それぞれが独自の当選確率を保持し、リストには勝手に統合されるのではなく見つかった重複件数が表示されます。知らせずに誰かの確率を半分にしてしまうような、求められていないお節介はいたしません。

アニメーションが果たす本来の役割

着地点があらかじめ決まっているアニメーションだからといって、偽物というわけではありません。ルーレットは実際に選ばれたセクターまで移動し、ポインターの刻み音は通過する仕切りと完全に同期しています。そして「ニアミス演出」を有効にした場合は、ランダムなセクターではなく当選者の隣のセクターでじらし、その後にすべてが始まる前に抽選されていた当選項目へと進みます。

ドキドキする臨場感は本物です。ただ、結果が見た目よりも一瞬早く決まっているだけなのです。