Розробка системи турнірів крипто-казино
Турніри — механіка утримання й монетизації. Замість (або в додавання до) звичайної PvH (player vs house), гравці змагаються за призовий фонд. Retention ефект значний: гравець, що бере участь в 7-денному турнірі, з високою ймовірністю грає кожен день. Для крипто-казино турніри з on-chain premio дають додаткову перевагу: верифіковні правила й виплати.
Технічно система турнірів — комбінація: накопичення очків (tracking), leaderboard (рейтинг), управління premio пулом (смарт-контракт), й дистрибьюція (автоматичні виплати).
Архітектура турнірної системи
Типи турнірів
Freeroll: безплатна участь, premio пул від казино. Engagement tool, не revenue generator. Привлекает нових гравців.
Buy-in: гравець платить entry fee (крипто), fees формують premio пул (мінус rake). Rake = 5–15% від total fees → дохід казино.
Leaderboard tournament: накопичуються очки за звичайну гру за період (7 днів, 24 години). Fixed premio пул від казино. Не вимагає окремої реєстрації — автоматично беруть участь усі гравці.
Sit & Go: починається коли набирається N гравців. Швидкі формати: 5–10 хвилин, $10 buy-in, топ 3 в грошах.
Prize pool смарт-контракт
contract TournamentManager {
struct Tournament {
bytes32 id;
TournamentType tournType;
uint256 startTime;
uint256 endTime;
uint256 prizePool; // total premio пул
uint256 entryFee; // 0 для freeroll
uint256 rake; // в basis points: 1000 = 10%
uint8 maxParticipants;
address[] participants;
bool distributed;
PrizeStructure[] prizes; // [{rank: 1, percent: 5000}, ...]
}
struct PrizeStructure {
uint8 rank;
uint16 percent; // basis points: 5000 = 50%
}
mapping(bytes32 => Tournament) public tournaments;
// Реєстрація з buy-in
function enterTournament(bytes32 tournamentId) external payable {
Tournament storage t = tournaments[tournamentId];
require(block.timestamp < t.startTime, "Tournament started");
require(t.participants.length < t.maxParticipants, "Full");
require(!_isParticipant(tournamentId, msg.sender), "Already entered");
if (t.entryFee > 0) {
require(msg.value == t.entryFee, "Wrong entry fee");
// Rake утримуємо одразу
uint256 rakeAmount = msg.value * t.rake / 10_000;
uint256 prizeContribution = msg.value - rakeAmount;
t.prizePool += prizeContribution;
// Rake → treasury
treasury.transfer(rakeAmount);
}
t.participants.push(msg.sender);
emit PlayerEntered(tournamentId, msg.sender);
}
// Дистрибьюція призів після закінчення (викликається оператором з результатами)
function distributePrizes(
bytes32 tournamentId,
address[] calldata rankedPlayers, // топ N гравців за очками
bytes calldata operatorSignature
) external {
Tournament storage t = tournaments[tournamentId];
require(block.timestamp > t.endTime, "Not ended");
require(!t.distributed, "Already distributed");
// Верифікуємо підпис оператора на ranked list
_verifyRankingSignature(tournamentId, rankedPlayers, operatorSignature);
t.distributed = true;
// Виплачуємо за prize structure
for (uint8 i = 0; i < t.prizes.length && i < rankedPlayers.length; i++) {
uint256 prize = t.prizePool * t.prizes[i].percent / 10_000;
payable(rankedPlayers[i]).transfer(prize);
emit PrizeAwarded(tournamentId, rankedPlayers[i], i + 1, prize);
}
}
}
Leaderboard й очки: off-chain з on-chain settlement
Оновлювати leaderboard on-chain при кожній ігровій дії — ірраціонально. Правильна архітектура: очки накопичуються off-chain (game server), settlement на on-chain тільки при фіналізації.
// Backend: Tournament Score Tracker
class TournamentScoreTracker {
private scores = new Map<string, Map<string, bigint>>(); // tournId → playerId → score
// Викликається при кожній ігровій транзакції
async onGameResult(event: GameResultEvent): Promise<void> {
const activeTournaments = await this.getActiveTournaments(event.timestamp);
for (const tournament of activeTournaments) {
if (!this.isEligible(event, tournament)) continue;
const points = this.calculatePoints(event, tournament.scoringRules);
const key = `${tournament.id}:${event.playerId}`;
const current = this.scores.get(tournament.id)?.get(event.playerId) ?? 0n;
this.scores.get(tournament.id)?.set(event.playerId, current + points);
// Публікуємо в Redis для real-time leaderboard API
await redis.zadd(
`tournament:${tournament.id}:leaderboard`,
{ score: Number(current + points), member: event.playerId },
);
}
}
// Фіналізуємо й готуємо для on-chain settlement
async finalizeAndSign(tournamentId: string): Promise<SignedRanking> {
const scores = this.scores.get(tournamentId);
if (!scores) throw new Error('Tournament not found');
// Сортуємо за очками
const ranked = Array.from(scores.entries())
.sort(([, a], [, b]) => (b > a ? 1 : -1))
.map(([playerId]) => playerId);
// Будуємо Merkle дерево з ranked list
const leaves = ranked.map((id, index) =>
keccak256(abi.encode(tournamentId, id, index + 1))
);
const merkleRoot = buildMerkleTree(leaves).getRoot();
// Підписуємо оператором
const signature = await operatorSigner.signMessage(
keccak256(abi.encode(tournamentId, merkleRoot, ranked.slice(0, 20)))
);
return { ranked: ranked.slice(0, 20), merkleRoot, signature };
}
}
Scoring rules — очки за різні ігри
Різні ігри дають різні очки залежно від правил турніру:
interface ScoringRules {
gameType: 'slots' | 'crash' | 'keno' | 'blackjack';
multiplier: number; // базовий множник очків
minBet?: bigint; // мінімальна ставка для зачету
bonusOnWin: number; // бонусні очки за виграш
bonusOnBigWin: number; // бонус за виграш > X
bigWinThreshold: number; // поріг "великого виграшу" в % від ставки
}
// Примерна формула:
function calculatePoints(event: GameResultEvent, rules: ScoringRules): bigint {
if (rules.minBet && event.betAmount < rules.minBet) return 0n;
// Базові очки = ставка × множник
let points = event.betAmount * BigInt(rules.multiplier) / 100n;
// Бонус за виграш
if (event.payout > event.betAmount) {
points += event.betAmount * BigInt(rules.bonusOnWin) / 100n;
// Бонус за великий виграш
const winRatio = Number(event.payout * 100n / event.betAmount);
if (winRatio >= rules.bigWinThreshold) {
points += event.betAmount * BigInt(rules.bonusOnBigWin) / 100n;
}
}
return points;
}
Real-time leaderboard API
Для хорошого UX: оновлення leaderboard кожні 10–30 секунд під час турніру.
// WebSocket endpoint для real-time leaderboard
wss.on('connection', (ws, req) => {
const tournamentId = extractTournamentId(req.url);
const sendLeaderboard = async () => {
// Redis Sorted Set: O(log N) для top-N запиту
const top20 = await redis.zrange(
`tournament:${tournamentId}:leaderboard`,
0, 19,
{ REV: true, WITHSCORES: true }
);
const leaderboard = top20.map(({ value: playerId, score }, index) => ({
rank: index + 1,
playerId,
score: BigInt(score),
displayName: playerNames.get(playerId),
prize: calculatePrize(tournamentId, index + 1),
}));
ws.send(JSON.stringify({ type: 'leaderboard_update', data: leaderboard }));
};
// Надсилаємо одразу й потім кожні 15 секунд
sendLeaderboard();
const interval = setInterval(sendLeaderboard, 15_000);
ws.on('close', () => clearInterval(interval));
});
Для гравця важливо бачити свою позицію: Redis підтримує ZRANK — O(log N) отримання рангу конкретного гравця.
Спеціальні механіки турнірів
Rebuy система
// Гравець може купити додаткові спроби (rebuys) в ранній фазі турніру
function rebuy(bytes32 tournamentId) external payable {
Tournament storage t = tournaments[tournamentId];
require(block.timestamp < t.rebuyDeadline, "Rebuy period ended");
require(msg.value == t.rebuyFee, "Wrong rebuy fee");
PlayerTournamentData storage pd = playerData[tournamentId][msg.sender];
require(pd.rebuys < t.maxRebuys, "Max rebuys reached");
pd.rebuys++;
uint256 rakeAmount = msg.value * t.rake / 10_000;
t.prizePool += msg.value - rakeAmount;
treasury.transfer(rakeAmount);
// Game server отримує сигнал: скинути очки гравця до стартових
emit RebuyPurchased(tournamentId, msg.sender, pd.rebuys);
}
Satellite турніри: вхід у крупні турніри через дрібні
Класична poker механіка: виграй в $5 сателліті → отримай билет на $100 турнір. У крипто: premio — NFT ticket або SBT (Soulbound Token), що дає право входу.
// Prize: NFT tournament ticket
function distributeSatellitePrizes(
bytes32 satelliteId,
address[] calldata winners,
bytes calldata signature
) external {
_verifyRankingSignature(satelliteId, winners, signature);
Tournament storage sat = tournaments[satelliteId];
require(block.timestamp > sat.endTime, "Not ended");
uint256 numTickets = sat.prizes.length; // тільки топ N отримують билети
for (uint256 i = 0; i < numTickets && i < winners.length; i++) {
// Мінтимо tournament ticket NFT
uint256 ticketId = ticketNFT.mint(winners[i], sat.targetTournamentId);
emit TicketAwarded(satelliteId, winners[i], ticketId, sat.targetTournamentId);
}
}
Fraud prevention й fairness
Collusion detection
У PvP турнірах гравці можуть домовлятися. On-chain методи обмежені, але:
- Rate limiting: не більше 1 ставки за N секунд в турнірних іграх
- IP/device fingerprinting на backend (off-chain)
- Обмеження: гравці з одного wallet cluster не можуть брати участь в одному турнірі
Anti-self-dealing
Оператор не повинен мати можливість маніпулювати результатами. Рішення: результати записуються від game server через GAME_SERVER_ROLE, ranking підписується оператором через multisig (не single signer).
// Багатопідписна фіналізація (2-of-3 серед операторів)
function finalizeRanking(
bytes32 tournamentId,
address[] calldata ranked,
bytes[] calldata signatures // 2 з 3 операторів
) external {
require(_verifyMultisig(tournamentId, ranked, signatures, 2), "Need 2 signatures");
_distributePrizes(tournamentId, ranked);
}
Стек розробки
Контракти: Solidity + Foundry. OpenZeppelin AccessControl. ERC-721 для tournament tickets. Backend: Node.js + TypeScript. Redis Sorted Sets для leaderboard. PostgreSQL для історії. Real-time: WebSocket сервер. Bull/BullMQ для фонових задач (score aggregation, finalization). Frontend: React + wagmi + Socket.io client.
| Компонент | Технологія |
|---|---|
| Prize pool | Solidity смарт-контракт |
| Score tracking | Redis Sorted Set |
| Leaderboard API | WebSocket + Redis ZRANGE |
| Ranking finalization | Operator multisig + Merkle |
| Tournament tickets | ERC-721 SBT |
Ориентири по срокам
MVP (leaderboard tournament, один тип гри, автоматична дистрибьюція): 4–5 тижнів. Повна система (всі типи турнірів, rebuy, сателліти, real-time leaderboard, fraud detection): 10–14 тижнів.







