Подтвердить что ты не робот

Перетасовка покерной колоды в 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 как семя в любом случае, и я не знаю, как я мог бы изменить его, чтобы это сделать.

4b9b3361

Ответ 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.

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