How provably fair works in Mines

Here's how to play Mines on Cloudbet, and how to check that every round's mine placements are provably fair.

How to play Mines

Every Mines round on Cloudbet uses the same provably fair foundation — server seed, client seed, nonce, and SHA3-256/SHAKE256 — covered in detail on our How Provably Fair Works hub page. This page covers what's specific to Mines: how that random number sequence becomes the actual grid of mines you play against.

Every Mines round on Cloudbet uses the same provably fair foundation — server seed, client seed, nonce, and SHA3-256/SHAKE256 — covered in detail on our How Provably Fair Works hub page. This page covers what's specific to Mines: how that random number sequence becomes the actual grid of mines you play against.

Turning random numbers into a mine grid

You choose the number of mines to place at the start of the game — we call this number N. The 5×5 Mines grid can be flattened out into a single row of 25 positions, each with its own index, starting at 1:

We sample a number from the random sequence (generated the same way described on the Provably Fair page to determine the position of the first mine:

If more mines need placing, we draw another random number and repeat — but now there are only 24 remaining spaces to choose from:

This repeats until every mine is placed. The full grid is then reconstructed and kept secret until the round ends:

Technical details: selecting each mine position

Before each pick, we know P — the number of mine-free positions left to choose from. P starts at 25 and drops by 1 each time a mine is placed.

A random 4-byte (32-bit) number is drawn from the SHAKE256 output for each mine, ranging from 0 to 4,294,967,295. To pick fairly among P positions, we apply rejection sampling (see the general concept explained on How Provably Fair Works, then take the result modulo P to get a placement index:

// Pseudo Code
const placementIndex = randomNumber % P;

If more mines remain, the chosen position is removed from the list, P drops by 1, and the process repeats.

Gameplay

The final mine-filled grid is built and hidden on the server before you make a move — it can't be changed or altered undetected afterward, thanks to the Commitment already shared with you. Each tile you pick is simply checked against this hidden grid.

Code for independent verification

This program is accurate to the Mines implementation on Cloudbet. Enter your Server Seed, Client Seed, Nonce, and number of mines, install the one dependency, and run it yourself to reconstruct your game's result.

Note: to reveal a completed game's Server Seed on Cloudbet, you need to have already started a new round (which rotates to a new seed pair).

Dependencies:

npm install js-sha3

The code:

// If you are running this yourself, then you can input your own data here
// You can then compare:
// 1) Cloudbet's provided Commitment to the Commitment outputted here. This proves that the Server Seed was not altered during gameplay.
// 2) Cloudbet's provided game outcome to the game outcome outputted here. This proves that your game's outcome was determined solely by the pre-game seeds and was not altered afterwards.
let myServerSeed      = 'your_server_seed'; // ✍️YOUR_INPUT✍️: Enter your (unhashed) Server Seed here
let myClientSeed      = 'your_client_seed'; // ✍️YOUR_INPUT✍️: Enter your Client Seed here
let myNonce           = 1;                  // ✍️YOUR_INPUT✍️: Enter your Nonce here
let myNumberOfMines   = 3;                  // ✍️YOUR_INPUT✍️: Enter your number of mines here
const usingCustomData = false;              // Change this to true if you are using your own data

// Load libraries
const crypto = require('crypto');
const { sha3_256, shake256 } = require('js-sha3');

// Step 1: Generate a Server Seed and Commitment
// And generate a Client Seed - either by player input or by the player's browser
function generateServerSeed() {
  let seed = crypto.randomBytes(32).toString('hex');
  if (usingCustomData) { seed = myServerSeed; }
  const commitment = sha3_256(seed);
  return { 'serverSeed': seed, 'commitment': commitment };
}
function getClientSeed() {
  let seed = crypto.randomBytes(32).toString('hex');
  if (usingCustomData) { seed = myClientSeed; }
  return seed;
}

// Step 2: Create a Round Signature with SHA3 using a Server Seed, Client Seed, and Nonce
function createRoundSignature(serverSeed, clientSeed, nonce) {
  return sha3_256(`${serverSeed}:${clientSeed}:${nonce}`);
}

// Step 3: Create a SHAKE256 stream generator from a Round Signature.
// To simplify this example program, this function doesn't create a stream, but instead a sequence of custom length.
function* createRandomNumberSequence(numbersToGenerate, roundSignature) {
  const numOfBytes = 4 * numbersToGenerate;           // 4 bytes (32 bits) per random number we generate
  const byteStream = shake256.create(8 * numOfBytes); // shaker256 takes bits as input
  byteStream.update(roundSignature);                  // Use the Round Signature to seed the generator
  const buf = Buffer.from(byteStream.digest());       // The Buffer class lets us package every 4 bytes into one 32-bit integer
  for (let i = 0; i < numbersToGenerate; i++) {
    yield buf.readUInt32BE(i * 4);                    // Read buf in 4-byte steps
  }
  throw new Error("No more numbers in the random number sequence! Fix: make 'numbersToGenerate' larger.");
}

// Step 4: Generate the mine locations
function pickUniquePositions(mineCount, totalTiles, rng) {
  const available = Array.from({ length: totalTiles }, (_, i) => i);
  let result = [];

  for (let i = 0; i < mineCount; ++i) {
    const index = sample(available.length, rng);

    result.push(available[index]);
    available.splice(index, 1);         // Remove chosen position from list of available positions
  }
  result.sort((a, b) => a - b);         // Sort the mine positions from smallest to largest
  result = result.map(num => num + 1);  // Increment positions by one so that they start with 1, 2, 3, ...
  return result;
}
// Rejection sampling for fair results
function sample(P, rng) {
  const maxAcceptable = Math.floor(0x100000000 / P) * P;

  // Rejection sampling
  let rand;
  do {
    rand = rng.next().value;
  } while (rand >= maxAcceptable);
  // Modulo operation
  const sampled_number = rand % P;

  return sampled_number;
}

// Example usage
(function main() {
  const gridSize = 25;
  let mineCount = 3;
  let nonce = 1;
  if (usingCustomData) { mineCount = myNumberOfMines; nonce = myNonce; }

  const { serverSeed, commitment } = generateServerSeed();
  const clientSeed = getClientSeed();

  const roundSignature = createRoundSignature(serverSeed, clientSeed, nonce);
  const numbersToGenerate = 24 * 2; // There are max. 24 mines, however due to rejection sampling we may need more random numbers
  const rng = createRandomNumberSequence(numbersToGenerate, roundSignature);

  const minePositions = pickUniquePositions(mineCount, gridSize, rng);

  console.log('📝 Commitment (shown before the game):', commitment);
  console.log('🔒 Server Seed (revealed after the game):', serverSeed);
  console.log('🎲 Client Seed:', clientSeed);
  console.log('🔢 Nonce:', nonce);
  console.log('🔁 Round Signature (can be verified after the game):', roundSignature);
  console.log('💣 Mine Positions (1 = top-left square, 5 = top-right square, 25 = bottom-right square):', minePositions);
})();

Try it now: test your luck on Mines

Want to see it live? Play a round of Cloudbet Mines — then verify your results with the provably fair calculator. It takes seconds. No technical know-how required.

You'll get to see the process in action, inspect your seeds, and watch how the tool reconstructs your result from scratch.

More provably fair game explainers:

  1. Provably fair Pump
  2. Provably fair Plinko
  3. Provably fair Dice
  4. Provably fair Dice: Golden Ticket
  5. Provably fair Limbo
  6. Provably fair Roulette