Login

A red exploding bomb with a skull on, the Thunderpick logo with a blue thunder bolt, the text Thunder Mines is Provably Fair in white and the subtext Test it yourself today in blue

Thunder Mines is Provably Fair

Reading Time: 6 minutes

Share this post:

What is Provably Fair?

Thunder Mines is a Thunderpick Original, a pixel-powered, retro-styled treasure hunt played on a 5×5 grid. Hidden beneath the tiles are mines; your goal is to uncover as many safe diamonds as possible before cashing out. Like all Thunderpick Originals, Thunder Mines uses a cryptographic provably fair system, which means you can independently verify that the outcome of every round was determined before you placed your bet and that neither you nor Thunderpick could manipulate the result.


How It Works

The fairness of every Thunder Mines round is guaranteed by a three-component seed system:

1. Server Seed

Before the round begins, the server generates a secret server seed and immediately commits to it by showing you its SHA-256 hash. This hash is a cryptographic fingerprint. It proves the server’s seed was fixed before you played, without revealing the seed itself.

2. Client Seed

By default your client seed is randomly assigned, but you can replace it with any string of your choice at any time before a new round begins. This gives you direct input into the outcome, ensuring that neither the server nor any third party can unilaterally determine the result. 

3. Nonce

Each bet you place increments a nonce (game counter) by one. The combination of server seed + client seed + nonce produces a unique, verifiable outcome for every single round.


Step-by-Step Verification

StepWhat happens
Before your betThe server generates a server seed and shows you its SHA-256 hash as a commitment.
During your betYour client seed and nonce are combined with the server seed to determine tile placement.
After the roundYou can reveal and verify that its SHA-256 hash matches what was shown before the game.
Independent checkUsing the script below, you can reproduce the exact mine positions yourself.

The Algorithm

The board layout is generated in two stages:

Stage 1 — Random Number Generation

The system computes HMAC-SHA256(key: serverSeed, message: “clientSeed:nonce:round”) for multiple rounds (⌈25/8⌉ = 4 rounds). Each 32-byte hash yields 8 floating-point numbers in [0, 1) by splitting the hash into 4-byte groups and applying positional byte weighting:

value = byte[0]/256 + byte[1]/256² + byte[2]/256³ + byte[3]/256⁴  

This produces 32 random numbers of which 25 are used. One for each tile on the 5×5 grid.

Stage 2 — Fisher-Yates Shuffle

The 25-element array (mines first, then crystals) is shuffled using the Fisher-Yates algorithm driven by the 25 random numbers. Starting with mines placed at positions [0 … mineCount-1] and crystals at [mineCount … 24], the shuffle iterates backwards from index 24 to 0, swapping each tile with a uniformly chosen earlier or equal position. This guarantees a perfectly uniform random permutation,  identical to the servers implementation.


Verify Your Round

Paste the script below into RunJS, fill in your four values from the game history, and run it. The printed 5×5 board must match exactly what you played.

// ============================================================
//  PROVABLY FAIR VERIFICATION
//  Mines Game — verify that your game result was not manipulated
//
//  HOW IT WORKS:
//  1. Before the game starts, the server commits to a secret
//     "server seed" (you see its SHA-256 hash up front).
//  2. You supply a "client seed" and a "nonce" (bet number).
//  3. After the game, you can reveal server seed by rotating seed pair
//  4. Paste this script into https://runjs.app/play and fill in
//     your four values below, then run it.
//
//  If the output matches what you played, the game was fair.
// ============================================================

// ── INPUTS ──────────────────────────────────────────────────
// Replace these four values with the ones from your game.

const serverSeed    = "33e91b21e223a141b5b5d5bfa2ea0594faa4e49bab1d542e748c149e5e3ae0d0";
const clientSeed    = "s0qbrrij836wy2mn";
const nonce         = 3619;  // your bet / round number
const numberOfMines = 5;     // how many mines were on the board

// ── STEP 1: Generate random numbers from the seeds ──────────

const randomNumbers = await generateRandomNumbers(serverSeed, clientSeed, nonce);

// ── STEP 2: Shuffle tiles using the random numbers ──────────

const board = shuffleTiles(numberOfMines, randomNumbers);

// ── STEP 3: Print the 5×5 board ─────────────────────────────

console.log("=== Board layout (5×5) ===\n");

for (let row = 0; row < 5; row++) {
    const rowTiles = board.slice(row * 5, row * 5 + 5);
    console.log(rowTiles.map(tile => tile.padEnd(10)).join(""));
}


// ── ALGORITHM ───────────────────────────────────────────────

/**
 * Returns an array of 25 random numbers in [0, 1) derived from
 * HMAC-SHA256(key: serverSeed, message: "clientSeed:nonce:round").
 *
 * One HMAC-SHA256 hash produces 32 bytes → 8 floats (4 bytes each).
 * For 25 tiles we need ⌈25/8⌉ = 4 rounds of hashing.
 */
async function generateRandomNumbers(serverSeed, clientSeed, nonce) {
    const normalizedSeed = serverSeed.toUpperCase().replaceAll(" ", "");
    const keyBytes       = new TextEncoder().encode(normalizedSeed);
    const messageBase    = `${clientSeed}:${nonce}:`;

    const cryptoKey = await crypto.subtle.importKey(
        "raw",
        keyBytes,
        { name: "HMAC", hash: "SHA-256" },
        false,
        ["sign"]
    );

    const randomNumbers  = new Array(25);
    const tilesPerHash   = 8;  // 32 bytes / 4 bytes per float
    const hashRoundsNeeded = Math.ceil(randomNumbers.length / tilesPerHash);

    let tileIndex = 0;

    for (let round = 0; round < hashRoundsNeeded; round++) {
        // Message changes each round so each hash is unique.
        const message    = messageBase + round;
        const messageBytes = new TextEncoder().encode(message);
        const hashBuffer   = await crypto.subtle.sign("HMAC", cryptoKey, messageBytes);
        const hashBytes    = new Uint8Array(hashBuffer);

        // Split the 32-byte hash into groups of 4 bytes.
        // Convert each group to a float in [0, 1) using positional weighting:
        //   value = byte0/256 + byte1/256² + byte2/256³ + byte3/256⁴
        // This gives ~32 bits of precision, identical to the server's calculation.
        const bytesPerFloat = 4;
        for (let byteOffset = 0; byteOffset < hashBytes.length; byteOffset += bytesPerFloat) {
            const value =
                hashBytes[byteOffset + 0] / 256 +
                hashBytes[byteOffset + 1] / (256 ** 2) +
                hashBytes[byteOffset + 2] / (256 ** 3) +
                hashBytes[byteOffset + 3] / (256 ** 4);

            randomNumbers[tileIndex] = value;
            tileIndex++;

            if (tileIndex === randomNumbers.length)
                break; // all tiles filled — stop early
        }
    }

    return randomNumbers;
}

/**
 * Places mineCount mines on a 25-tile board using a
 * Fisher-Yates shuffle driven by randomNumbers.
 *
 * Fisher-Yates works backwards: for each position i (from 24 down to 0),
 * pick a random position j in [0, i] and swap tiles[i] with tiles[j].
 * This produces a perfectly uniform random permutation.
 */
function shuffleTiles(mineCount, randomNumbers) {
    // Start with an ordered list: mines first, then safe tiles.
    const tiles = [
        ...Array(mineCount).fill("BOMB"),
        ...Array(25 - mineCount).fill("CRYSTAL"),
    ];

    // Fisher-Yates shuffle — identical to the server's implementation.
    for (let i = 24; i >= 0; i--) {
        const j = Math.floor(randomNumbers[i] * (i + 1));
        [tiles[i], tiles[j]] = [tiles[j], tiles[i]];
    }

    return tiles;
}

Open in RunJS — paste the code, replace the four input values with your own, and run.


Where to Find Your Values

All these parameters can be checked for each Wager in the Provably Fair expandable menu, which can be accessed in the following way:

Open “Profile menu” -> “My bets”:

Switch to Casino tab and choose Mines wager

Expand Provably Fair menu:

In the menu User is able to find Hash prepared from Server Seed, Client Seed, and Nonce.

To rotate seeds you can use the “Rotate your seed pair” feature:

Here, not only the currently used pair of seeds are displayed, but also the User is already informed about the next Server Seed (of course in Hashed form) and can Edit client seed.

After given pair of Seeds is rotated, the not-hashed Server Seed from previous pair is revealed to Player:

In the result, players have access to all parameters, which allows them to check if a given Wager was actually randomly generated.


Thunder Mines uses the same provably fair infrastructure as all Thunderpick Originals. The algorithm is open, auditable, and reproducible by anyone with a browser.

Scroll to Top