シャッフルではないもの
JavaScriptにおける最も一般的なランダム化バグ:
const shuffled = items.sort(() => Math.random() - 0.5); // 間違い
これは一見問題なく見え、あらゆる場所で使われています。しかし、これはシャッフルではありません。ソートの比較関数は一貫性が求められますが、これはそうではないため、エンジンのソートは偏った順列を生成します。
4つのクイズ選択肢で測定した結果:
| 位置 | 正解がそこに配置される確率 |
|---|---|
| 0 | 28.3% |
| 1 | 28.2% |
| 2 | 24.7% |
| 3 | 18.8% |
一様ならそれぞれ25%のはずです。実際には、常に最初の選択肢を選ぶ方がランダムに選ぶよりも良い結果が出ました。これに気づいたプレイヤーは有利になり、クイズは静かに不公平になっていました。
修正方法
Fisher–Yatesシャッフルを、ヘルパー関数として一度だけ実装します:
function shuffle(a) {
const out = [...a];
for (let i = out.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[out[i], out[j]] = [out[j], out[i]];
}
return out;
}
そして、すべての該当箇所を置き換えます。このバグは通常、複数回出現します。あるコードベースでは11箇所で見つかりました。クイズの選択肢のシャッフル、問題の選出、提供されるアイテムの選択、メニューの順序付けなど、それぞれが少しずつ偏りを生んでいました。
パターンマッチで一律に置き換えないでください。 (Math.random() - 0.5) は物理演算のノイズやジッターとして正当に使用されることもあります:
velocity += (Math.random() - 0.5) * 0.012; // これはそのままに
各該当箇所が実際にソートの比較関数であることを確認してください。
分布をテストする
ランダム性を目視で確認しないでください。カウントしましょう:
const pos = [0,0,0,0], N = 200000;
for (let i = 0; i < N; i++) {
const order = shuffle(['a','b','c','d'].map((t,i) => [t,i]));
pos[order.findIndex(o => o[1] === 0)]++;
}
pos.forEach((c,i) => console.log(i, (c/N*100).toFixed(1) + '%'));
各位置の割合が約3パーセントポイント以上広がっている場合、何かが偏っていることを意味します。
テストには適切な生成器を使用してください。 手作りの線形合同法生成器では1~2ポイントの「誤差」が生じ、実際のバグのように見えましたが、それはテスト自身の問題でした。Math.random() は分布の確認には十分に一様です。カスタム生成器は再現可能なシードが必要な場合に温存し、別途検証してください。
表示する確率を確認する
どこかに確率を表示する場合(パーセンテージ、プログレスバー、成功確率など)、それがコードが実際にロールする値と一致していることを確認してください。実際のロールを数十万回シミュレートして比較します:
cred 0 shown 5.0%/40.0%/55.0% actual 5.0%/40.0%/55.0%
cred 100 shown 40.0%/40.0%/20.0% actual 40.0%/40.1%/20.0%
最大誤差が0.5パーセントポイント未満であれば、表示は嘘をついていません。結果が反映されない表示確率は、何も表示しないより悪いです。なぜなら、人々はそれに基づいて意思決定を行うからです。
ついでに確認すること
表示された目標が実際に達成可能かどうか確認してください。カウンターが「3/5」と表示している場合、プレイヤーには5が目標であることを示しています。もし5が発生する確率が0.17%しかない場合、インターフェースは実質的に決して起こらないことを約束していることになります。表示するすべてのマイルストーンの実際の確率を計算してください。