Shuffling a poker deck in JavaScript with window.crypto.getRandomValues

前端 未结 3 1244
死守一世寂寞
死守一世寂寞 2021-02-05 08:32

A poker deck has 52 cards and thus 52! or roughly 2^226 possible permutations.

Now I want to shuffle such a deck of cards perfectly, with truly

相关标签:
3条回答
  • 2021-02-05 09:01

    Here's a function I wrote that uses Fisher-Yates shuffling based on random bytes sourced from window.crypto. Since Fisher-Yates requires that random numbers are generated over varying ranges, it starts out with a 6-bit mask (mask=0x3f), but gradually reduces the number of bits in this mask as the required range gets smaller (i.e., whenever i is a power of 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;
    }
    

    An assessment of window.crypto libraries really deserves its own question, but anyway...

    The pseudorandom stream provided by window.crypto.getRandomValues() should be sufficiently random for any purpose, but is generated by different mechanisms in different browsers. According to a 2013 survey:

    • Firefox (v. 21+) uses NIST SP 800-90 with a 440-bit seed. Note: This standard was updated in 2015 to remove the (possibly backdoored) Dual_EC_DRBG elliptic curve PRNG algorithm.

    • Internet Explorer (v. 11+) uses one of the algorithms supported by BCryptGenRandom (seed length = ?)

    • Safari, Chrome and Opera use an ARC4 stream cipher with a 1024-bit seed.


    Edit:

    A cleaner solution would be to add a generic shuffle() method to Javascript's array prototype:

    // 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();
    
    0 讨论(0)
  • 2021-02-05 09:05

    Combining this answer from here with this answer from another question, it seems the following could be a more general and modular (though less optimized) version:

    // 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;
    }
    
    0 讨论(0)
  • 2021-02-05 09:17

    I personally think you could move outside the box a little bit. If you're that worried about randomness, you could look into an API key from random.org ( https://api.random.org/json-rpc/1/ ) or parse it out of a link like this: https://www.random.org/integer-sets/?sets=1&num=52&min=1&max=52&seqnos=on&commas=on&order=index&format=html&rnd=new .

    Sure, your datasets could be intercepted, but if you get a few hundred thousand sets of them then shuffle those sets you would be fine.

    0 讨论(0)
提交回复
热议问题