Перетасовка покерной колоды в JavaScript с окном.криптографический.getRandomValues
покер колода имеет 52 карты и таким образом 52!
или около 2^226
возможных перестановок.
почему это действительно необходимо?
для игр, возможно, вам действительно не нужна идеальная случайность, если только нужно выиграть деньги. Кроме того, люди, вероятно, даже не заметят "различий" в случайности.
но если я не ошибаюсь, если вы используете функции перетасовки и компоненты RNG, обычно встроенные в популярные языки программирования, вы часто получаете не более 32 бит энтропии и 2^32
государства. Таким образом, вы никогда не сможете достичь всех 52!
возможные перестановки колоды при перетасовке, но только о ...
0.000000000000000000000000000000000000000000000000000000005324900157 %
... о возможных перестановках. Это означает, что много всех возможных игр, которые мог бы играть или моделироваться в теории никогда не будет видно на практике.
кстати, вы можете еще больше улучшить результаты, если вы не сбрасываете порядок по умолчанию каждый раз перед перетасовкой, а вместо этого начинаете с порядка из последнего перетасовки или сохранить "беспорядок" после игры был сыгран и перетасован оттуда.
требования:
Итак, чтобы сделать то, что описано выше, нужны все три составляющие, насколько я понял:
- хороший алгоритм перетасовки, который обеспечивает равномерное распределение.
- правильный RNG С по крайней мере 226 битами внутреннего состояния. Поскольку мы находимся на детерминированных машинах, PRNG будет всем, что мы получим, и возможно, это должен быть CSPRNG.
- случайное семя с по крайней мере 226 битами энтропии.
решения:
теперь это достижимо? Что у нас есть?
- Фишер-Йейтс перемешать будет хорошо, насколько я вижу.
- на xorshift7 RNG имеет больше, чем требуется 226 бит внутреннего состояния и должно быть достаточно.
- используя
window.crypto.getRandomValues
мы можем генерировать необходимые 226 бит энтропии для использования в качестве нашего семени. Если этого все еще недостаточно, мы можем добавить еще немного энтропии из других источников.
вопрос:
решения (и требования), упомянутые выше правильные? Как вы можете реализовать перетасовку, используя эти решения на JavaScript на практике? Как объединить эти три компонента в рабочее решение?
Я думаю, я должен заменить использование Math.random
в Примере Фишер-Йейтс перетасовать с вызовом xorshift7. Но этот RNG выводит значение в [0, 1)
диапазон поплавка, и мне нужно [1, n]
целое число, а не диапазон. При масштабировании этого диапазона я не хочу терять равномерное распределение. Более того, я хотел около 226 бит случайности. Если мой RNG выводит только один Number
, разве эта случайность не сведена к 2^53 (или 2^64) битам, потому что больше нет возможностей для вывода?
In чтобы сгенерировать семя для RNG, я хотел сделать что-то вроде этого:
var randomBytes = generateRandomBytes(226);
function generateRandomBytes(n) {
var data = new Uint8Array(
Math.ceil(n / 8)
);
window.crypto.getRandomValues(data);
return data;
}
это правильно? Я не вижу, как я мог бы пройти randomBytes
RNG как семя в любом случае, и я не знаю, как я мог бы изменить его, чтобы получить это.
3 ответов
вот функция, которую я написал, которая использует перетасовку Фишера-Йейтса на основе случайных байтов, полученных из window.crypto
. Поскольку Фишер-Йейтс требует, чтобы случайные числа генерировались в разных диапазонах, он начинается с 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 (ст. 21+) использует NIST SP 800-90 с 440-битным семенем. Примечание: этот стандарт был обновлен в 2015 году, чтобы удалить (возможно, backdoored)
Dual_EC_DRBG
эллиптическая кривая PRNG алгоритм.Internet Explorer (ст. 11+) использует один из алгоритмов, поддерживаемых BCryptGenRandom длина (семя = ?)
Safari, Chrome и Opera использовать ARC4 потоковый шифр с 1024-битным семян.
Edit:
более чистым решением было бы добавить общий shuffle()
метод массива Javascript прототип:
// Add Fisher-Yates shuffle method to Javascript's 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();
объединение ответ отсюда ответ С еще вопрос, кажется, следующее Может быть более общей и модульной (хотя и менее оптимизированной) версией:
// 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;
}
Я лично думаю, что вы могли бы немного выйти за рамки коробки. Если вы беспокоитесь о случайности, вы можете посмотреть в ключ 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 .
конечно, ваши наборы данных могут быть перехвачены, но если вы получите несколько сотен тысяч наборов из них тогда перетасуйте эти наборы, и все будет в порядке.