Перейти к основному содержимому
Все статьи
Как это устроено

Колесо уже знает ответ

Почему победитель определяется до начала вращения и какие преимущества это даёт.

Нажмите «Крутить», и колесо будет вращаться десять секунд. Кажется, будто результат решается именно в эти десять секунд — в замедлении, в последнем полуобороте, в том, перескочит ли стрелка ещё один разделитель.

Но это не так. Победитель определяется в первую очередь, за считанные микросекунды, а затем анимация просто наводится на него.

Зачем такой порядок

Очевидная альтернатива — раскрутить колесо и зафиксировать результат там, где оно остановится. Кажется, что так честнее: никто не выбирал, всё решила физика. Но это гораздо сложнее обосновать, поскольку любой вопрос о честности превращается в вопрос об анимации.

Можно ли крутануть сильнее и изменить результат? Вносит ли досрочный клик во время вращения искажения в итог? Что происходит у пользователя с включённой настройкой 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, и он получит три доли при розыгрыше вместо одной. На колесе он будет отображаться как более широкий сектор, а в списке покажется процент, который этот вес даёт в реальности, чтобы никому не приходилось делить вручную.

Два пункта с одинаковым текстом остаются двумя отдельными пунктами. Каждый из них сохраняет свой шанс, а в списке выводится количество найденных повторов вместо их тихого объединения: уменьшение чьих-то шансов вдвое без предупреждения — сомнительная услуга, о которой никто не просил.

Зачем тогда нужна анимация

Целенаправленная анимация — не значит фальшивая. Колесо действительно проходит весь путь до выбранного сектора, щелчки точно синхронизированы с проходимыми разделителями, а замирание перед почти выпавшим сектором (если оно включено) останавливается на соседнем с победителем варианте, а не на случайном, после чего доходит до пункта, который был определён ещё до старта.

Интрига настоящая. Просто результат известен чуть раньше, чем кажется.