In this post I explore how to simulate the shuffling of a deck of cards, addressing the challenges of randomness and predictability in a solidity smart contract environment where every input and output is part of the public record.
Whether you’re looking to create an on-chain poker game, a collectible card game, or you’re simply curious about random processes on the blockchain, this guide will teach you the basics to ‘shuffle’ through your blockchain development journey effectively. So fire up remix, and lets deal the cards!
UPDATE: 1st August 2024
Note there is a more advanced dealing mechanism here which handles private card distribution and verification. Read about the challenges and possible solutions here: https://github.com/jamesbachini/Decentralized-Poker?tab=readme-ov-file#challenges-and-solutions
The Solidity Shuffle Code
The full code for this smart contract is available on the Solidity Snippets github repository: https://github.com/jamesbachini/Solidity-Snippets/blob/main/contracts/Shuffle.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract Shuffle {
struct Card {
uint8 suit; // 1-4
uint8 rank; // 1-13
}
Card[] public deck;
constructor() {
// Create a deck of cards
for (uint8 suit = 1; suit <= 4; suit++) {
for (uint8 rank = 1; rank <= 13; rank++) {
deck.push(Card(suit, rank));
}
}
}
function shuffle() public {
uint256 deckSize = deck.length;
for (uint256 i = 0; i < deckSize; i++) {
uint256 j = uint256(keccak256(abi.encode(block.prevrandao, i))) % deckSize;
Card memory tmpCard = deck[i];
deck[i] = deck[j];
deck[j] = tmpCard;
}
}
}
The Explainer
This contract is for a deck of cards, each with a suit (1-4) and a rank (1-13), and provides a method to shuffle those cards.
The contract starts with defining a Card
struct that has two properties, suit
and rank
. These are each 8-bit unsigned integers, which means they can hold values from 0 to 255.
suit
is expected to have the value from 1 to 4, representing the four suits in a deck of cards. Rank represents the 13 ranks in a deck, from 1 (Ace) to 13 (King).
The constructor for the contract, which is run once when the contract is deployed, initializes the deck
array. It does this by looping over each possible combination of suit (1-4) and rank (1-13) and pushing a new Card
with that suit and rank onto the deck.
At this point the cards are in order and we now need a shuffle
function, which can be called by anyone to mix up the order within the array.
For each card in the deck (starting from the first card and going to the last one), it calculates a pseudorandom index j
. Then it swaps the card at the current index with the card at the pseudorandom index. The pseudorandom index is calculated by taking the keccak256 hash of the block’s randao and the current index, converting that hash to an unsigned integer, and taking the modulus of that with the deck size.
This algorithm is known as the Fisher-Yates shuffle and ensures each permutation of the deck is equally likely if the used random number generator has a uniform distribution.
Note this simple implementation might be subject to validator manipulation because the randomness is somewhat dependent on the block builder. It could be improved by using a oracle as a source of randomness, something like Chainlink’s VRF, although this then adds a layer of centralization and trust to the protocol.
Another consideration is the predictiveness of the deck. In most card games the cards and order of the shuffle are concealed from the players but this difficult to do on a public blockchain network where all transactions and state are transparent.
Maybe there is a solution to a private shuffle mechanism using zero-knowledge tech but that is beyond the scope of this article and perhaps something to play with in the future.