Розробка гри Roulette на блокчейні
Рулетка на блокчейні — класичний use case для provably fair гемблингу. Головна інженерна задача: забезпечити непередбачуваність результату, яку ніхто не може маніпулювати — ні казино ні гравець. Рішення — verifiable randomness через Chainlink VRF або commit-reveal схему.
Randomness: головна проблема
Не можна використовувати block.timestamp, block.prevrandao, або block hashes як джерело randomness. Miner/validator може впливати на ці значення — miner extractable value (MEV) атака на randomness. Для рулетки: нода може вибирати включати або ні транзакцію залежно від winning block.
Chainlink VRF v2.5
Криптографічно безпечна, verifiable random від decentralized oracle network:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract RouletteGame is VRFConsumerBaseV2Plus {
uint256 public subscriptionId;
bytes32 public keyHash;
enum BetType { Number, Red, Black, Even, Odd, Low, High }
struct Bet {
address player;
BetType betType;
uint8 number; // для Number ставки (0-36)
uint256 amount;
bool settled;
}
struct Round {
uint256 requestId;
uint8 result; // 0-36
bool fulfilled;
mapping(uint256 => Bet) bets;
uint256 betCount;
}
mapping(uint256 => Round) public rounds;
uint256 public currentRoundId;
uint256 public constant MAX_BET = 1 ether;
uint256 public constant HOUSE_EDGE = 270; // 2.7%
function placeBet(BetType betType, uint8 number) external payable {
require(msg.value > 0 && msg.value <= MAX_BET, "Invalid bet amount");
if (betType == BetType.Number) {
require(number <= 36, "Invalid number");
}
Round storage round = rounds[currentRoundId];
uint256 betId = round.betCount++;
round.bets[betId] = Bet({
player: msg.sender,
betType: betType,
number: number,
amount: msg.value,
settled: false
});
}
// Закрити раунд і запросити randomness
function spinWheel() external returns (uint256 requestId) {
requestId = s_vrfCoordinator.requestRandomWords(
VRFV2PlusClient.RandomWordsRequest({
keyHash: keyHash,
subId: subscriptionId,
requestConfirmations: 3,
callbackGasLimit: 300_000,
numWords: 1,
extraArgs: VRFV2PlusClient._argsToBytes(
VRFV2PlusClient.ExtraArgsV1({ nativePayment: false })
)
})
);
requestToRound[requestId] = currentRoundId;
rounds[currentRoundId].requestId = requestId;
currentRoundId++;
}
// Callback від Chainlink VRF
function fulfillRandomWords(
uint256 requestId,
uint256[] calldata randomWords
) internal override {
uint256 roundId = requestToRound[requestId];
Round storage round = rounds[roundId];
// 0-36 включаючи = 37 значень
uint8 result = uint8(randomWords[0] % 37);
round.result = result;
round.fulfilled = true;
_settleAllBets(roundId);
}
function _settleAllBets(uint256 roundId) internal {
Round storage round = rounds[roundId];
for (uint256 i = 0; i < round.betCount; i++) {
Bet storage bet = round.bets[i];
if (bet.settled) continue;
bet.settled = true;
uint256 payout = _calculatePayout(bet, round.result);
if (payout > 0) {
payable(bet.player).transfer(payout);
}
}
}
function _calculatePayout(
Bet memory bet,
uint8 result
) internal pure returns (uint256) {
bool win = false;
uint256 multiplier = 0;
if (bet.betType == BetType.Number) {
win = (bet.number == result);
multiplier = 35; // 35:1
} else if (bet.betType == BetType.Red) {
win = _isRed(result);
multiplier = 1;
} else if (bet.betType == BetType.Black) {
win = (!_isRed(result) && result != 0);
multiplier = 1;
} else if (bet.betType == BetType.Even) {
win = (result != 0 && result % 2 == 0);
multiplier = 1;
} else if (bet.betType == BetType.Odd) {
win = (result % 2 == 1);
multiplier = 1;
} else if (bet.betType == BetType.Low) {
win = (result >= 1 && result <= 18);
multiplier = 1;
} else if (bet.betType == BetType.High) {
win = (result >= 19 && result <= 36);
multiplier = 1;
}
if (!win) return 0;
return bet.amount + (bet.amount * multiplier);
}
// Червоні числа європейської рулетки
function _isRed(uint8 n) internal pure returns (bool) {
uint256 redNumbers = 0x3A4A5251412C2B1A191009080706;
return (redNumbers >> n) & 1 == 1;
}
}
Frontend і анімація
Саме колесо рулетки — DOM canvas (React + Pixi.js або Three.js). Ключові моменти:
- Анімація колеса запускається при виклику
spinWheel(), ще до отримання результату від VRF - Реальний результат приходить за ~30–60 секунд (час VRF callback)
- Колесо крутиться кілька повних оборотів, потім плавно тормозить на потрібному секторі
- Event
RoundSettledвід контракту — триггер для фінальної позиції
Мережі і стоимість
Основний параметр: стоимість VRF callback. Дорого на Ethereum mainnet. Рекомендовані мережі:
| Мережа | VRF Cost | Час ответа | Рекомендація |
|---|---|---|---|
| Arbitrum | ~$0.30–0.80 | ~30–60 сек | Оптимально |
| Polygon | ~$0.01–0.05 | ~30–60 сек | Бюджетно |
| Avalanche | ~$0.10–0.30 | ~30–60 сек | Добре |
| Base | ~$0.05–0.20 | ~30–60 сек | Растуча |
Для high-frequency спинів (кілька в хвилину) — розглянути alternative VRF: Pyth Entropy (значно дешевше), або commit-reveal (миттєво але менш децентралізовано).







