Колесо уже знает ответ
Почему победитель определяется до начала вращения и какие преимущества это даёт.
Нажмите «Крутить», и колесо будет вращаться десять секунд. Кажется, будто результат решается именно в эти десять секунд — в замедлении, в последнем полуобороте, в том, перескочит ли стрелка ещё один разделитель.
Но это не так. Победитель определяется в первую очередь, за считанные микросекунды, а затем анимация просто наводится на него.
Зачем такой порядок
Очевидная альтернатива — раскрутить колесо и зафиксировать результат там, где оно остановится. Кажется, что так честнее: никто не выбирал, всё решила физика. Но это гораздо сложнее обосновать, поскольку любой вопрос о честности превращается в вопрос об анимации.
Можно ли крутануть сильнее и изменить результат? Вносит ли досрочный клик во время вращения
искажения в итог? Что происходит у пользователя с включённой настройкой
prefers-reduced-motion, который вообще не видит анимацию?
Если определять результат сразу, на все три вопроса будет один ответ: нет, потому что розыгрыш уже состоялся. Сильный жест добавляет лишние обороты. Ранний клик сокращает путь. Режим уменьшения движения пропускает анимацию полностью и сразу объявляет результат. Три разных сценария внутри одного блока кода вместо трёх независимых веток, честность каждой из которых пришлось бы доказывать отдельно.
Как устроен розыгрыш
Источник энтропии — crypto.getRandomValues, собственный криптографический
генератор браузера, и ни в коем случае не Math.random.
Получение равномерно распределённого числа требует на один шаг больше действий, чем кажется
на первый взгляд. Привычное выражение draw % count даёт смещение каждый раз, когда count не делит
2³² без остатка: первые несколько вариантов получают один дополнительный шанс из четырёх миллиардов.
Мелочь, но это всё же перекос, а в честном колесе
его быть не должно. Поэтому значения, попадающие в остаточный «хвост», отбрасываются,
и генерируется новое число.
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, и он получит три доли при розыгрыше вместо одной. На колесе он будет отображаться как более широкий сектор, а в списке покажется процент, который этот вес даёт в реальности, чтобы никому не приходилось делить вручную.
Два пункта с одинаковым текстом остаются двумя отдельными пунктами. Каждый из них сохраняет свой шанс, а в списке выводится количество найденных повторов вместо их тихого объединения: уменьшение чьих-то шансов вдвое без предупреждения — сомнительная услуга, о которой никто не просил.
Зачем тогда нужна анимация
Целенаправленная анимация — не значит фальшивая. Колесо действительно проходит весь путь до выбранного сектора, щелчки точно синхронизированы с проходимыми разделителями, а замирание перед почти выпавшим сектором (если оно включено) останавливается на соседнем с победителем варианте, а не на случайном, после чего доходит до пункта, который был определён ещё до старта.
Интрига настоящая. Просто результат известен чуть раньше, чем кажется.