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
| Step | What happens |
| Before your bet | The server generates a server seed and shows you its SHA-256 hash as a commitment. |
| During your bet | Your client seed and nonce are combined with the server seed to determine tile placement. |
| After the round | You can reveal and verify that its SHA-256 hash matches what was shown before the game. |
| Independent check | Using 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.