Перетасовка покерной колоды в JavaScript с помощью window.crypto.getRandomValues
Покерная колода имеет 52 карты и, следовательно, 52!
или примерно 2^226
возможные перестановки.
Теперь я хочу перетасовать такую колоду карт отлично, с действительно случайными результатами и равномерным распределением, чтобы вы могли достичь каждой из этих возможных перестановок, и каждая из них будет в равной степени появляться.
Почему это действительно необходимо?
Для игр, возможно, вам не нужна идеальная случайность, если нет денег, которые нужно выиграть. Кроме того, люди, вероятно, даже не будут воспринимать "различия" в случайности.
Но если я не ошибаюсь, если вы используете функции перетасовки и компоненты RNG, обычно встроенные в популярные языки программирования, вы часто получаете не более 32 бит состояний энтропии и 2^32
. Таким образом, вы никогда не сможете достичь всех 52!
возможных перестановок колоды при перетасовке, но только о...
0.0000000000000000000000000000000000000000000000000000000000005324900157%
... возможных подстановок. Это означает, что многие из возможных игр, которые могут быть воспроизведены или смоделированы в теории, на практике никогда не будут на практике.
Кстати, вы можете еще больше улучшить результаты, если вы не reset в порядке по умолчанию каждый раз перед перетасовкой, а вместо этого начинаете с порядка с последнего перетасовки или сохраняете "беспорядок" после того, как игра была играл и перетасовывал оттуда.
Требования:
Итак, чтобы сделать то, что описано выше, нужно, насколько я понял, все три следующих компонента:
- Хороший алгоритм перетасовки, обеспечивающий равномерное распределение.
- Собственный RNG с не менее чем 226 битами внутреннего состояния. Поскольку мы находимся на детерминированных машинах, PRNG будет всем, что мы получим, и, возможно, это должно быть CSPRNG.
- Случайное семя с не менее чем 226 бит энтропии.
Решения:
Теперь это достижимо? Что у нас есть?
- Перетасовка Fisher-Yates будет, насколько я понимаю, прекрасной.
- xorshift7 RNG имеет более чем 226 бит внутреннего состояния и должен быть достаточным.
- Используя
window.crypto.getRandomValues
, мы можем генерировать требуемые 226 бит энтропии, которые будут использоваться в качестве нашего семени. Если этого еще недостаточно, мы можем добавить еще несколько энтропий из других источников.
Вопрос:
Правильны ли упомянутые выше решения (а также требования)? Как вы можете реализовать перетасовку с использованием этих решений в JavaScript на практике? Как объединить три компонента с рабочим решением?
Я думаю, мне нужно заменить использование Math.random
в примере Shuffle Fisher-Yates вызовом xorshift7. Но этот RNG выводит значение в диапазоне [0, 1)
float, и вместо этого мне нужен целочисленный диапазон [1, n]
. При масштабировании этого диапазона я не хочу потерять равномерное распределение. Более того, я хотел около 226 бит случайности. Если мой RNG выводит только один Number
, разве эта случайность не уменьшается до 2 ^ 53 (или 2 ^ 64) битов, потому что больше нет возможностей для вывода?
Чтобы генерировать семя для RNG, я хотел сделать что-то вроде этого:
var randomBytes = generateRandomBytes(226);
function generateRandomBytes(n) {
var data = new Uint8Array(
Math.ceil(n / 8)
);
window.crypto.getRandomValues(data);
return data;
}
Это правильно? Я не вижу, как я мог бы передать randomBytes
в RNG как семя в любом случае, и я не знаю, как я мог бы изменить его, чтобы это сделать.
Ответы
Ответ 1
Здесь функция, которую я написал, использует переключение Fisher-Yates на основе случайных байтов, полученных из window.crypto
. Поскольку Fisher-Yates требует, чтобы случайные числа генерировались в разных диапазонах, он начинается с 6-битной маски (mask=0x3f
), но постепенно уменьшает количество бит в этой маске, поскольку требуемый диапазон становится меньше (т.е. Когда i
- степень 2).
function shuffledeck() {
var cards = Array("A♣️","2♣️","3♣️","4♣️","5♣️","6♣️","7♣️","8♣️","9♣️","10♣️","J♣️","Q♣️","K♣️",
"A♦️","2♦️","3♦️","4♦️","5♦️","6♦️","7♦️","8♦️","9♦️","10♦️","J♦️","Q♦️","K♦️",
"A♥️","2♥️","3♥️","4♥️","5♥️","6♥️","7♥️","8♥️","9♥️","10♥️","J♥️","Q♥️","K♥️",
"A♠️","2♠️","3♠️","4♠️","5♠️","6♠️","7♠️","8♠️","9♠️","10♠️","J♠️","Q♠️","K♠️");
var rndbytes = new Uint8Array(100);
var i, j, r=100, tmp, mask=0x3f;
/* Fisher-Yates shuffle, using uniform random values from window.crypto */
for (i=51; i>0; i--) {
if ((i & (i+1)) == 0) mask >>= 1;
do {
/* Fetch random values in 100-byte blocks. (We probably only need to do */
/* this once.) The `mask` variable extracts the required number of bits */
/* for efficient discarding of random numbers that are too large. */
if (r == 100) {
window.crypto.getRandomValues(rndbytes);
r = 0;
}
j = rndbytes[r++] & mask;
} while (j > i);
/* Swap cards[i] and cards[j] */
tmp = cards[i];
cards[i] = cards[j];
cards[j] = tmp;
}
return cards;
}
Оценка библиотек window.crypto
действительно заслуживает собственного вопроса, но в любом случае...
Псевдослучайный поток, предоставляемый window.crypto.getRandomValues()
, должен быть достаточно случайным для любых целей, но генерируется различными механизмами в разных браузерах. Согласно опросу 2013 года:
-
Firefox (v. 21+) использует NIST SP 800-90 с 440-битным семена. Примечание. Этот стандарт был обновлен в 2015 году, чтобы удалить алгоритм PRNG с эллиптической кривой Dual_EC_DRBG
(возможно, backdoored).
-
Internet Explorer (v. 11+) использует один из алгоритмов, поддерживаемых BCryptGenRandom (семя длина =?)
-
Safari, Chrome и Opera используют ARC4 потоковый шифр с 1024-битным семенем.
Изменить:
Более чистым решением было бы добавить общий метод shuffle()
к прототипу массива Javascript:
// Add Fisher-Yates shuffle method to Javascript Array type, using
// window.crypto.getRandomValues as a source of randomness.
if (Uint8Array && window.crypto && window.crypto.getRandomValues) {
Array.prototype.shuffle = function() {
var n = this.length;
// If array has <2 items, there is nothing to do
if (n < 2) return this;
// Reject arrays with >= 2**31 items
if (n > 0x7fffffff) throw "ArrayTooLong";
var i, j, r=n*2, tmp, mask;
// Fetch (2*length) random values
var rnd_words = new Uint32Array(r);
// Create a mask to filter these values
for (i=n, mask=0; i; i>>=1) mask = (mask << 1) | 1;
// Perform Fisher-Yates shuffle
for (i=n-1; i>0; i--) {
if ((i & (i+1)) == 0) mask >>= 1;
do {
if (r == n*2) {
// Refresh random values if all used up
window.crypto.getRandomValues(rnd_words);
r = 0;
}
j = rnd_words[r++] & mask;
} while (j > i);
tmp = this[i];
this[i] = this[j];
this[j] = tmp;
}
return this;
}
} else throw "Unsupported";
// Example:
deck = [ "A♣️","2♣️","3♣️","4♣️","5♣️","6♣️","7♣️","8♣️","9♣️","10♣️","J♣️","Q♣️","K♣️",
"A♦️","2♦️","3♦️","4♦️","5♦️","6♦️","7♦️","8♦️","9♦️","10♦️","J♦️","Q♦️","K♦️",
"A♥️","2♥️","3♥️","4♥️","5♥️","6♥️","7♥️","8♥️","9♥️","10♥️","J♥️","Q♥️","K♥️",
"A♠️","2♠️","3♠️","4♠️","5♠️","6♠️","7♠️","8♠️","9♠️","10♠️","J♠️","Q♠️","K♠️"];
deck.shuffle();
Ответ 2
Объединив этот ответ отсюда этот ответ от другой вопрос, кажется, что следующая более общая и модульная (хотя и менее оптимизированная) версия:
// Fisher-Yates
function shuffle(array) {
var i, j;
for (i = array.length - 1; i > 0; i--) {
j = randomInt(0, i + 1);
swap(array, i, j);
}
}
// replacement for:
// Math.floor(Math.random() * (max - min)) + min
function randomInt(min, max) {
var range = max - min;
var bytesNeeded = Math.ceil(Math.log2(range) / 8);
var randomBytes = new Uint8Array(bytesNeeded);
var maximumRange = Math.pow(Math.pow(2, 8), bytesNeeded);
var extendedRange = Math.floor(maximumRange / range) * range;
var i, randomInteger;
while (true) {
window.crypto.getRandomValues(randomBytes);
randomInteger = 0;
for (i = 0; i < bytesNeeded; i++) {
randomInteger <<= 8;
randomInteger += randomBytes[i];
}
if (randomInteger < extendedRange) {
randomInteger %= range;
return min + randomInteger;
}
}
}
function swap(array, first, second) {
var temp;
temp = array[first];
array[first] = array[second];
array[second] = temp;
}
Ответ 3
Я лично думаю, что вы можете немного переместиться за пределы коробки. Если вас беспокоит случайность, вы можете заглянуть в ключ API от random.org(https://api.random.org/json-rpc/1/) или проанализировать его из ссылка вроде этого: https://www.random.org/integer-sets/?sets=1&num=52&min=1&max=52&seqnos=on&commas=on&order=index&format=html&rnd=new.
Конечно, ваши наборы данных могут быть перехвачены, но если вы получите несколько сотен тысяч наборов из них, то перетасовать эти наборы вам будет хорошо.