Blockchain Tower Game Development
Tower is a high-risk climbing game: a player ascends through levels, selecting one cell per level while avoiding the "mine". Higher levels yield larger multipliers. At any moment, players can cashout and collect their winnings. Structurally similar to Mines, but with progressive betting growth.
Blockchain Tower is interesting because it requires fair randomness at each level independently, while preventing players from knowing the mine location on the next level in advance.
Fair Randomness Architecture
The key challenge: where is the mine on each level? On-chain data is public — if mine placement is stored in contract state, players can read it before acting.
Approach 1: Commit-Reveal per level
The operator/oracle generates a seed hash for each level before game start, publishes it on-chain, and reveals the seed only after the player makes their choice:
struct TowerGame {
address player;
uint256 bet;
uint8 currentLevel; // current level (0 = start)
uint8 maxLevels; // tower height
uint256 currentMultiplier; // x1000 for precision
bytes32 serverSeedHash; // server seed hash
bool active;
}
The challenge: requires a backend that plays honestly (cannot place mine retroactively). Solution — publish hash before game start. If the server reveals a seed mismatching the hash — the violation is verifiable.
Approach 2: Chainlink VRF per game
Request one large random number at game start, deterministically derive mine position for each level:
mapping(uint256 => TowerGame) public games; // requestId → game
function startTower(uint8 levels, uint8 cellsPerLevel) external payable {
require(msg.value >= MIN_BET);
require(levels >= 3 && levels <= 10);
require(cellsPerLevel >= 2 && cellsPerLevel <= 5);
uint256 requestId = s_vrfCoordinator.requestRandomWords(
VRFV2PlusClient.RandomWordsRequest({
keyHash: KEY_HASH,
subId: subscriptionId,
requestConfirmations: 3,
callbackGasLimit: 200000,
numWords: 1,
extraArgs: VRFV2PlusClient._argsToBytes(
VRFV2PlusClient.ExtraArgsV1({nativePayment: false})
)
})
);
games[requestId] = TowerGame({
player: msg.sender,
bet: msg.value,
currentLevel: 0,
maxLevels: levels,
currentMultiplier: 1000, // x1.0
gameSeed: 0, // filled during fulfillment
active: false
});
}
function fulfillRandomWords(uint256 requestId, uint256[] calldata randomWords) internal override {
TowerGame storage game = games[requestId];
game.gameSeed = randomWords[0];
game.active = true;
emit TowerReady(requestId, game.player);
}
// Get mine position for level (only when move is made!)
function _getMinePosition(uint256 requestId, uint8 level, uint8 cellsPerLevel) private view returns (uint8) {
return uint8(uint256(keccak256(abi.encodePacked(
games[requestId].gameSeed,
level
))) % cellsPerLevel);
}
_getMinePosition is a private view. Technically readable if you know gameSeed. But gameSeed is stored in state... and public again.
Solution: seed concealment via hashing
Store only keccak256(gameSeed) in events, seed itself only as transaction parameter, not in storage. Imperfect, but raises the bar for cheating: requires monitoring pending transactions.
Practical production solution: hybrid — Chainlink VRF for unreadable seed + storing only seed hash publicly. Mine position revealed via event only after player move, not stored until action.
Multipliers and Mathematics
Each tower level with n cells and one mine: probability of safe selection = (n-1)/n. Mathematically fair multiplier after k levels:
multiplier(k) = product_{i=1}^{k} (n_i / (n_i - 1))
For 5-level tower, 3 cells: each level ×(3/2) = ×1.5. After 5 levels: 1.5^5 ≈ 7.59x. House edge added via coefficient:
// Multiplier table (x1000, 3 cells, 2% house edge)
uint256[10] public multipliers3Cells = [
0, // level 0
1470, // x1.47 (fair 1.5 * 0.98)
2161, // x2.16
3177, // x3.18
4670, // x4.67
6865, // x6.87
10092, // x10.09
14835, // x14.84
21807, // x21.81
32056 // x32.06
];
function selectCell(uint256 gameId, uint8 cellIndex) external {
TowerGame storage game = games[gameId];
require(game.player == msg.sender && game.active);
require(cellIndex < cellsPerLevel);
uint8 minePosition = _getMinePosition(gameId, game.currentLevel, cellsPerLevel);
if (cellIndex == minePosition) {
// Hit mine — lose stake
game.active = false;
emit GameLost(gameId, msg.sender, game.currentLevel, minePosition);
// ETH stays in contract (bankroll)
} else {
// Passed level — update multiplier
game.currentMultiplier = multipliers[game.currentLevel + 1];
game.currentLevel++;
if (game.currentLevel == game.maxLevels) {
// Beat tower — automatic cashout
_payout(game);
} else {
emit LevelCleared(gameId, game.currentLevel, game.currentMultiplier);
}
}
}
function cashout(uint256 gameId) external {
TowerGame storage game = games[gameId];
require(game.player == msg.sender && game.active && game.currentLevel > 0);
_payout(game);
}
function _payout(TowerGame storage game) private {
uint256 payout = game.bet * game.currentMultiplier / 1000;
game.active = false;
(bool success, ) = game.player.call{value: payout}("");
require(success, "Transfer failed");
emit GameWon(msg.sender, payout, game.currentLevel);
}
Frontend: Animations and UX
Tower game is visually simple, but UX is critical: climbing animation, multiplier pulsing, cashout button always available.
Async flow: VRF fulfillment awaited via TowerReady event polling. After — each move is instant on-chain transaction (no additional VRF).
// wagmi hook for awaiting game start
const { data: gameReadyEvent } = useWatchContractEvent({
address: TOWER_ADDRESS,
abi: TOWER_ABI,
eventName: 'TowerReady',
args: { player: address },
onLogs: (logs) => {
const gameId = logs[0].args.requestId
setActiveGameId(gameId)
setGameState('playing')
}
})
Progressive multiplier disclosure: animate multiplier growth on each successful level — key retention moment. Current potential payout should be prominently visible, in real-time.
Bankroll and Limits
Maximum payout capped by bankroll. Check before accepting bet:
function maxWinForBet(uint256 bet) public view returns (uint256) {
return bet * multipliers[maxLevels] / 1000;
}
modifier bankrollSufficient(uint256 bet) {
require(address(this).balance >= maxWinForBet(bet) + bet, "Insufficient bankroll");
_;
}
Stack and Timeline
| Component | Technology |
|---|---|
| Contract | Solidity + Chainlink VRF |
| Tests | Foundry + VRF mock |
| Frontend | React + wagmi |
| Network | Arbitrum / Polygon |
Basic Tower game (smart contract, tests, UI): 3-4 weeks. With advanced visuals, stats, leaderboard: 6-8 weeks. Smart contract audit mandatory — contract manages game bankroll.







