Как работает рандомайзер

Рандомизатор получает поток битов от математического алгоритма или физического шума, а затем переводит его в число, пункт списка либо новый порядок элементов. Большинство программ выдают псевдослучайные значения: для игр и бытового выбора этого хватает, а для паролей и шифрования нужен криптографический генератор с качественным начальным состоянием.

Как работает рандомайзер

Дальше вы проследите путь случайного числа от шума и начального состояния до готового результата, поймёте разницу между обычным и криптографическим генератором, а в конце соберёте собственный рандомизатор в одном HTML-файле.

В статье вы узнаете:

Что происходит после нажатия кнопки

Рандомизатор не достаёт готовое число из цифровой шляпы. Он решает две отдельные задачи. Сначала получает достаточно непредсказуемое значение, затем переводит его в форму, которая нужна человеку: целое число от 1 до 100, одну строку из списка или перестановку всех участников.

Путь результата обычно состоит из четырёх шагов. Программа проверяет входные данные и составляет множество допустимых вариантов. Генератор выдаёт порцию битов. Следующий алгоритм равномерно укладывает это большое число в нужный диапазон. Последнее правило решает, можно ли повторять элементы, сколько их взять и нужно ли сохранить весь набор.

Неровный сигнал поступает в генератор, из которого выходят карточки с числами 7, 2, 9 и 4
Слева направо: исходный шум или внутреннее состояние поступает в генератор, алгоритм меняет это состояние и выдаёт поток значений, а прикладное правило превращает поток в нужный результат.

Представим список из четырёх фильмов. Чтобы выбрать один, программе достаточно получить индекс от 0 до 3. Чтобы выстроить очередь просмотра, одного индекса мало: нужно последовательно менять фильмы местами так, чтобы любой из 24 возможных порядков имел одинаковый шанс. Источник случайности может быть общим, но прикладные алгоритмы различаются.

Поэтому фраза «этот сервис использует хорошие случайные числа» ещё ничего не говорит о результате. Сильный источник можно испортить неверным переводом в диапазон, а правильный алгоритм перемешивания не спасёт предсказуемое начальное состояние.

Почему псевдослучайность не является обманом

Обычный процессор выполняет точные команды. Если дать алгоритму одинаковое начальное состояние, он повторит одинаковую последовательность. Такие генераторы называют псевдослучайными, или PRNG: внутри всё определяется формулой, но без знания состояния результат выглядит хаотичным.

Посмотрим на игрушечный генератор. Он хранит число от 0 до 99 и после каждого запроса применяет одну формулу:

Новое состояние = (старое состояние × 17 + 43) mod 100

Если начать с 12, получим 47, затем 42, 57 и снова 12. Цикл из четырёх значений легко предсказать. Настоящие PRNG устроены сложнее, хранят гораздо больше битов состояния и могут пройти огромный путь до повтора, но принцип остаётся прежним.

Начальное значение называют seed, по-русски обычно говорят «зерно» или «начальное состояние». Оно полезно не только как секрет. Разработчик может сохранить seed мира в игре и воспроизвести тот же ландшафт. Учёный может повторить симуляцию с той же последовательностью. Тестировщик может вернуть редкую ошибку, которая возникла на двадцать седьмом случайном шаге. Полная воспроизводимость здесь является преимуществом.

Функция Math.random() в JavaScript возвращает дробь от 0 включительно до 1 не включительно. Стандарт ECMAScript требует приблизительно равномерное распределение, но оставляет конкретный алгоритм разработчикам среды. Поэтому разные браузеры и версии могут выдавать разные последовательности, а сама функция не обещает стойкость против человека, который пытается угадать следующее значение.

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

Где компьютер берёт начальный хаос

Чтобы детерминированная программа стала непредсказуемой, ей нужен внешний материал. Его называют энтропией. В этом контексте энтропия показывает не беспорядок на рабочем столе, а неопределённость: сколько информации не хватает наблюдателю, чтобы угадать результат.

Операционная система может собирать вариации времени аппаратных событий, джиттер генераторов тактовой частоты, электрический или тепловой шум и данные специальных аппаратных модулей. Движения мыши и интервалы между нажатиями клавиш тоже исторически использовали как дополнительный материал, но простое время в миллисекундах или координата курсора сами по себе слишком предсказуемы.

Сырой физический сигнал редко даёт аккуратные нули и единицы с шансом ровно 50 на 50. На него влияют температура, помехи, особенности датчика и даже старение детали. Поэтому источник проверяют на отказ, оценивают минимальную энтропию и пропускают данные через функцию обработки. Рекомендация NIST SP 800-90B разделяет шумовой источник, проверки работоспособности и обработку в одну систему получения энтропии.

Затем небольшой объём качественной энтропии загружает криптографически стойкий псевдослучайный генератор, или CSPRNG. Он быстро разворачивает начальное состояние в длинный поток, но устроен так, чтобы по известным выходам было практически невозможно восстановить внутреннее состояние и предсказать следующие биты. Система может периодически добавлять новую энтропию.

Браузерный метод crypto.getRandomValues() работает именно в этой логике. Он не измеряет новый фотон при каждом вызове. Браузер обращается к криптографическому генератору, который опирается на источник операционной системы. Это быстрее прямого чтения физического шума и достаточно надёжно для большинства прикладных задач.

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

В 2018 году исследователи провели фотонный тест Белла, который позволял проверить сам источник по наблюдаемым квантовым корреляциям. После 55 110 210 испытаний они извлекли 1024 бита, равномерность которых была подтверждена с точностью до 10−12. Огромный объём работы ради 128 байт хорошо показывает цену не просто случайности, а её физической сертификации [Bierhorst et al., 2018].

Какие числа можно считать случайными

У случайности нет одного переключателя «настоящая» или «ненастоящая». Практический генератор оценивают по нескольким свойствам, и сильный результат по одному из них не гарантирует остальные.

Равномерность. У каждого допустимого варианта должен быть одинаковый шанс, если задача не задаёт другие веса. Для числа от 1 до 10 это вероятность 1/10. Для перестановки четырёх разных строк это вероятность 1/24 для каждого порядка.

Независимость. Предыдущие результаты не должны сдвигать следующий выбор без явного правила. Пять выпавших шестёрок не заставляют честный кубик «компенсировать» серию единицей. Небольшая выборка вообще любит серии и пропуски, хотя человеку они кажутся подозрительными.

Непредсказуемость. Наблюдатель не должен вычислить следующий результат по предыдущим. Последовательность цифр числа π может выглядеть хаотично и проходить многие статистические проверки, но она полностью определена. Достаточно знать позицию, и загадка исчезает.

Проверяемость. Для публичного розыгрыша мало показать скриншот победителя. Нужны заранее зафиксированный список, правила удаления дублей, момент выбора, источник случайности и запись процедуры. Иногда используют опубликованный seed или внешний маяк случайности, чтобы любой участник мог повторить вычисление.

Статистические тесты помогают заметить явный перекос, слишком частые серии и зависимости между битами. Они не доказывают, что поток был непредсказуем до появления. Файл из миллиона заранее записанных честных бросков пройдёт тесты, но человек с копией файла назовёт каждый следующий результат.

Практическое правило. Для игры или выбора фильма важнее равномерность. Для моделирования добавляется воспроизводимость. Для паролей и токенов нужна непредсказуемость. Для конкурса с призом нужна ещё и проверяемая процедура. Слово «рандом» не заменяет эти требования.

Как выбирают числа, пункты и порядок списка

Генератор обычно отдаёт большое целое число или дробь, а человеку нужен маленький понятный результат. На этом последнем переходе и появляются разные виды рандомизаторов.

Число из диапазона. Если генератор выдал равномерную дробь u от 0 до 1, целое число от a до b включительно можно получить так:

Результат = floor(u × (b − a + 1)) + a

Функция floor отбрасывает дробную часть. При a = 1 и b = 6 отрезок от 0 до 1 делится на шесть равных частей, как виртуальный кубик. По этой схеме работает выбор случайных чисел из диапазона: можно указать границы, количество результатов и исключения. Текущая числовая версия использует Math.random(), поэтому подходит для обычного выбора, но не для секретных ключей.

Один или несколько пунктов. Список из n строк нумеруется от 0 до n − 1, затем генератор выбирает индекс. Если повторы разрешены, каждый выбор выполняется заново. Если они запрещены, удобно перемешать копию списка и взять первые k строк. Так устроен случайный выбор из списка.

Новый порядок всего списка. Здесь используют алгоритм Фишера-Йетса. Он идёт с последнего элемента к первому, на каждом шаге выбирает любую позицию от начала до текущей и меняет элементы местами. При равномерном выборе индекса каждая перестановка получает одинаковый шанс. Этот алгоритм можно увидеть в работе, если перемешать строки списка.

Популярный короткий трюк array.sort(() => Math.random() - 0.5) хуже. Сортировка ждёт согласованное правило сравнения, а случайный ответ нарушает его. Количество и порядок сравнений зависят от реализации, поэтому одни перестановки могут появляться чаще других. Фишер-Йетс длиннее на несколько строк, зато его поведение понятно.

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

Есть ещё одна маленькая ловушка. Браузер может выдать любое 32-битное число от 0 до 4 294 967 295. Если взять остаток от деления на 10, диапазон из 232 значений делится на десять с остатком 6. Первые шесть вариантов получат по одному лишнему исходному значению.

Разница крошечная, но исправляется просто: отбросить верхний неполный хвост и запросить число заново. Списковые инструменты eCalc получают значения через crypto.getRandomValues() и используют именно такое отбрасывание перед делением. Хорошая случайность здесь состоит не из одного магического вызова, а из нескольких аккуратных решений подряд.

Как написать свой рандомизатор на HTML и JavaScript

Создайте текстовый файл randomizer.html, вставьте код ниже и откройте файл двойным щелчком. Страница работает локально без сервера и выбирает целое число между двумя границами включительно.

<!doctype html>
<html lang="ru">
<meta charset="utf-8">
<title>Мой рандомизатор</title>

<label>От <input id="min" type="number" value="1"></label>
<label>До <input id="max" type="number" value="100"></label>
<button id="run" type="button">Выбрать</button>
<p id="result">Здесь появится число</p>

<script>
const minInput = document.querySelector('#min');
const maxInput = document.querySelector('#max');
const runButton = document.querySelector('#run');
const result = document.querySelector('#result');

function randomInt(min, max) {
  const size = max - min + 1;

  if (!Number.isSafeInteger(size) || size < 1 || size > 2 ** 32) {
    throw new Error('Диапазон должен содержать от 1 до 2³² чисел');
  }

  const fullRange = 2 ** 32;
  const limit = fullRange - (fullRange % size);
  const data = new Uint32Array(1);

  do {
    crypto.getRandomValues(data);
  } while (data[0] >= limit);

  return min + (data[0] % size);
}

runButton.addEventListener('click', () => {
  const min = Math.ceil(Number(minInput.value));
  const max = Math.floor(Number(maxInput.value));

  if (!Number.isSafeInteger(min) || !Number.isSafeInteger(max) || min > max) {
    result.textContent = 'Проверьте границы диапазона';
    return;
  }

  try {
    result.textContent = String(randomInt(min, max));
  } catch (error) {
    result.textContent = error.message;
  }
});
</script>
</html>

Функция запрашивает одно беззнаковое 32-битное число. Переменная limit отсекает неполный хвост, поэтому остаток от деления распределяется равномерно. После этого к остатку прибавляется нижняя граница. Код поддерживает и отрицательные числа, пока весь диапазон содержит не больше 232 вариантов.

Для самого короткого учебного примера можно было написать Math.floor(Math.random() * size) + min. Здесь намеренно использован более аккуратный вариант: он знакомит с crypto.getRandomValues() и сразу показывает место, где часто появляется малозаметный перекос.

Откройте файл, выберите диапазон от 1 до 10 и нажмите кнопку двадцать раз. Какие-то значения встретятся несколько раз подряд, другие могут не появиться вообще. Это нормальное поведение маленькой случайной выборки, а не просьба срочно чинить вселенную. Затем попробуйте сохранить последовательность и подумайте, какие свойства нужны вашей задаче: равномерность, воспроизводимость, секретность или публичная проверка.

Что хочется сказать в конце. Современный рандомизатор обычно соединяет физическую неопределённость и строгую математику. Шум даёт непредсказуемое начало, генератор быстро производит поток битов, а последний алгоритм честно переводит его в число, выбор или перестановку. Случайность появляется не в одной строке кода, а в том, насколько хорошо состыкованы все три части.

Есть что добавить?

Напишите своё мнение, комментарий или предложение.