Blockchain Mines Game Development
Mines (or Minesweeper casino variant) is a game on N×N field where mines and safe cells are hidden. Player opens cells one by one, each safe cell increases multiplier. Can take winnings at any time or continue risking — until hitting a mine. Element of choice makes game significantly more engaging than Dice.
Mathematics of Mines
Standard field: 5×5 = 25 cells. Let mineCount = 5 (20% chance of mine on each opened cell).
Probability of safely opening k cells:
P(k safe) = ∏(i=0 to k-1) [(25 - mines - i) / (25 - i)]
With 5 mines, open 1 cell safely: (25-5)/25 = 80%. Open 2 in a row: 80% × (19/24) = 63.3%. Open 5 in a row: ~33%.
Multiplier at k opened cells = 1 / P(k) × (1 - houseEdge).
This creates exponentially growing risk/reward — exactly what makes Mines psychologically engaging.
Smart Contract: Reveal Pattern
Key complexity of Mines on blockchain: can't store mine positions on-chain until game ends (player sees them). Solution: commit-reveal.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract BlockchainMines is VRFConsumerBaseV2Plus {
struct Game {
address player;
uint8 fieldSize; // 5 for 5x5
uint8 mineCount;
uint256 betAmount;
uint256 currentMultiplier; // in basis points
uint8 openedCells;
uint256 vrfSeed; // from VRF, stored encrypted until cashout
bytes32 minesSeedHash; // hash(vrfSeed) — public
GameStatus status;
bool[25] openedCellMap; // which cells opened
}
enum GameStatus { WAITING_VRF, ACTIVE, CASHED_OUT, BUSTED }
mapping(uint256 => Game) public games;
mapping(address => uint256) public activeGame;
// Multiplier table: [fieldSize][mineCount][openedCells] → multiplier
// Pre-calculated off-chain, loaded at deploy
mapping(uint8 => mapping(uint8 => mapping(uint8 => uint256))) public multiplierTable;
function startGame(uint8 mineCount) external payable returns (uint256 gameId) {
require(activeGame[msg.sender] == 0, "Game already active");
require(mineCount >= 1 && mineCount <= 24, "Invalid mine count");
require(msg.value >= MIN_BET, "Bet too low");
gameId = ++gameCounter;
games[gameId] = Game({
player: msg.sender,
fieldSize: 5,
mineCount: mineCount,
betAmount: msg.value,
currentMultiplier: 10000,
openedCells: 0,
vrfSeed: 0,
minesSeedHash: 0,
status: GameStatus.WAITING_VRF,
openedCellMap: [false, false, /*...*/ false],
});
activeGame[msg.sender] = gameId;
// Request VRF for mine position seed
uint256 vrfRequestId = _requestVRF();
vrfToGame[vrfRequestId] = gameId;
}
function fulfillRandomWords(uint256 requestId, uint256[] calldata randomWords)
internal override
{
uint256 gameId = vrfToGame[requestId];
Game storage game = games[gameId];
// Store seed — don't reveal mine positions to player
// Encrypt via xor with secret key (can reveal after game)
game.vrfSeed = randomWords[0]; // in production — encrypt
game.minesSeedHash = keccak256(abi.encodePacked(randomWords[0]));
game.status = GameStatus.ACTIVE;
emit GameStarted(gameId, game.minesSeedHash);
}
function openCell(uint256 gameId, uint8 cellIndex) external {
Game storage game = games[gameId];
require(game.player == msg.sender, "Not your game");
require(game.status == GameStatus.ACTIVE, "Game not active");
require(cellIndex < 25, "Invalid cell");
require(!game.openedCellMap[cellIndex], "Already opened");
game.openedCellMap[cellIndex] = true;
// Determine if this cell has mine
bool isMine = _isMine(game.vrfSeed, game.mineCount, cellIndex, game.fieldSize);
if (isMine) {
game.status = GameStatus.BUSTED;
activeGame[msg.sender] = 0;
// Reveal all mines
uint8[] memory minePositions = _getMinePositions(game.vrfSeed, game.mineCount);
emit GameBusted(gameId, cellIndex, minePositions);
} else {
game.openedCells++;
game.currentMultiplier = multiplierTable[game.fieldSize][game.mineCount][game.openedCells];
emit CellOpened(gameId, cellIndex, game.currentMultiplier);
}
}
function cashout(uint256 gameId) external {
Game storage game = games[gameId];
require(game.player == msg.sender, "Not your game");
require(game.status == GameStatus.ACTIVE, "Game not active");
require(game.openedCells > 0, "No cells opened");
game.status = GameStatus.CASHED_OUT;
activeGame[msg.sender] = 0;
uint256 payout = (game.betAmount * game.currentMultiplier) / 10000;
payable(msg.sender).transfer(payout);
emit GameCashedOut(gameId, game.openedCells, game.currentMultiplier, payout);
}
// Determine mine positions from seed
function _getMinePositions(uint256 seed, uint8 mineCount)
internal pure returns (uint8[] memory positions)
{
positions = new uint8[](mineCount);
bool[25] memory placed;
uint256 minesPlaced = 0;
uint256 i = 0;
while (minesPlaced < mineCount) {
uint8 pos = uint8(uint256(keccak256(abi.encodePacked(seed, i))) % 25);
if (!placed[pos]) {
placed[pos] = true;
positions[minesPlaced] = pos;
minesPlaced++;
}
i++;
}
}
function _isMine(
uint256 seed,
uint8 mineCount,
uint8 cellIndex,
uint8 fieldSize
) internal pure returns (bool) {
uint8[] memory minePositions = _getMinePositions(seed, mineCount);
for (uint i = 0; i < minePositions.length; i++) {
if (minePositions[i] == cellIndex) return true;
}
return false;
}
}
Multiplier Table
Multipliers are pre-calculated mathematically and loaded into contract at deploy. Example for 5×5 field with 3 mines:
| Opened | Multiplier (1% edge) |
|---|---|
| 1 | 1.14x |
| 2 | 1.32x |
| 3 | 1.56x |
| 5 | 2.22x |
| 10 | 6.60x |
| 15 | 27.3x |
| 22 | 990x |
Animation and UX
Mines requires good visual feedback:
- Explosion on mine hit
- Progressive glow/intensification on successful cells
- Building tension in sound design
- Instant visible Cashout button
Development of Mines: smart contract + VRF + frontend — 4-5 weeks. Mathematically correct multiplier table and animated UI included.







