공정하지 않은 셔플
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;
}
그런 다음 모든 곳에서 교체하세요. 이 버그는 보통 한 번 이상 나타납니다. 어떤 코드베이스에서는 열한 군데에서 발견되었습니다. 퀴즈 보기 섞기, 어떤 질문을 보여줄지 고르기, 어떤 항목을 제공할지 선택하기, 메뉴 순서 정하기 등이었고 각각 약간씩 분포를 왜곡했습니다.
패턴 매칭만으로 무분별하게 대체하지 마십시오. (Math.random() - 0.5)는 물리 시뮬레이션의
노이즈나 지터(jitter) 등 합법적인 용도로도 사용됩니다:
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) + '%'));
위치 간 퍼짐(Spread)이 약 3% 포인트 이상이면 무언가 편향되어 있다는 의미입니다.
테스트에는 적절한 생성기를 사용하십시오. 직접 만든 선형 합동 생성기는 1–2 포인트의 "오차"를
만들어내어 실제 버그처럼 보이지만 실제로는 테스트 자체의 문제였습니다. Math.random()은 분포
검사에 충분히 균일합니다. 사용자 정의 생성기는 재현 가능한 시드가 필요할 때 사용하고,
별도로 검증하십시오.
사용자에게 보여주는 확률 확인
어디에나 확률(백분율, 진행률 표시줄, 성공 확률)을 표시한다면, 그것이 코드가 실제로 굴리는 값과 일치하는지 확인하십시오. 실제 굴림을 수십만 번 시뮬레이션하여 비교하세요:
cred 0 표시 5.0%/40.0%/55.0% 실제 5.0%/40.0%/55.0%
cred 100 표시 40.0%/40.0%/20.0% 실제 40.0%/40.1%/20.0%
최대 오차가 0.5% 포인트 미만이면 표시가 거짓말을 하고 있지 않다는 의미입니다. 결과가 실제로 지켜지지 않는 확률을 표시하는 것은 아무것도 표시하지 않는 것보다 더 나쁩니다. 사람들이 그 정보를 바탕으로 결정을 내리기 때문입니다.
추가 확인 사항
명시된 목표가 실제로 도달 가능한지 확인하십시오. "5개 중 3개"라고 읽히는 카운터는 플레이어에게 5가 목표라고 알려줍니다. 만약 5가 0.17%의 확률로만 발생한다면, 인터페이스는 사실상 절대 일어나지 않는 일을 약속하는 것입니다. 표시하는 모든 이정표의 실제 확률을 계산해 보십시오.