Перетасовка покерной колоды в JavaScript с окном.криптографический.getRandomValues

покер колода имеет 52 карты и таким образом 52! или около 2^226 возможных перестановок.

почему это действительно необходимо?

для игр, возможно, вам действительно не нужна идеальная случайность, если только нужно выиграть деньги. Кроме того, люди, вероятно, даже не заметят "различий" в случайности.

но если я не ошибаюсь, если вы используете функции перетасовки и компоненты RNG, обычно встроенные в популярные языки программирования, вы часто получаете не более 32 бит энтропии и 2^32 государства. Таким образом, вы никогда не сможете достичь всех 52! возможные перестановки колоды при перетасовке, но только о ...

0.000000000000000000000000000000000000000000000000000000005324900157 %

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

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

требования:

Итак, чтобы сделать то, что описано выше, нужны все три составляющие, насколько я понял:

  1. хороший алгоритм перетасовки, который обеспечивает равномерное распределение.
  2. правильный RNG С по крайней мере 226 битами внутреннего состояния. Поскольку мы находимся на детерминированных машинах, PRNG будет всем, что мы получим, и возможно, это должен быть CSPRNG.
  3. случайное семя с по крайней мере 226 битами энтропии.

решения:

теперь это достижимо? Что у нас есть?

  1. Фишер-Йейтс перемешать будет хорошо, насколько я вижу.
  2. на xorshift7 RNG имеет больше, чем требуется 226 бит внутреннего состояния и должно быть достаточно.
  3. используя 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 .

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