import crypto from 'node:crypto'; import { Users, Games } from './db.js'; import { pushFeed } from './realtime.js'; const HOUSE_EDGE = 0.99; // 1% Hausvorteil const rounds = new Map(); // roundId -> state function rid() { return crypto.randomBytes(12).toString('hex'); } function rand() { return crypto.randomBytes(4).readUInt32BE(0) / 0xffffffff; } function round(n, d = 2) { const f = Math.pow(10, d); return Math.round(n * f) / f; } function debit(userId, amount) { const u = Users.byId(userId); if (!u) return { ok: false, error: 'User fehlt' }; if (amount <= 0) return { ok: false, error: 'Einsatz muss > 0 sein' }; if (u.balance < amount) return { ok: false, error: 'Nicht genug Guthaben' }; Users.setBalance(userId, u.balance - amount); return { ok: true, balance: u.balance - amount }; } function credit(userId, amount) { return Users.addBalance(userId, amount); } function settle(userId, game, bet, payout, result) { Games.add(userId, game, bet, payout, result); const u = Users.byId(userId); if (payout >= bet * 5 && payout > 0) { pushFeed('win', `${u?.username || 'Spieler'} gewinnt ${round(payout)} 🪙 bei ${game} (${round(payout / bet, 2)}x)`); } return Users.byId(userId)?.balance; } // ============ CHICKEN ROAD ============ // Huhn ueberquert Fahrbahnen. Jede Spur erhoeht den Multiplikator, // aber es droht ein Crash (Auto). Cashout jederzeit moeglich. const CHICKEN_DIFF = { easy: { p: 0.04, lanes: 24 }, medium: { p: 0.10, lanes: 20 }, hard: { p: 0.20, lanes: 16 }, insane: { p: 0.35, lanes: 12 }, }; function chickenMultipliers(p, lanes) { // Faire Multiplikatoren: 1/(1-p)^k * HouseEdge const arr = []; for (let k = 1; k <= lanes; k++) { arr.push(round(HOUSE_EDGE / Math.pow(1 - p, k), 2)); } return arr; } export function chickenStart(userId, bet, difficulty = 'medium') { bet = Number(bet); const diff = CHICKEN_DIFF[difficulty] || CHICKEN_DIFF.medium; const d = debit(userId, bet); if (!d.ok) return d; const mults = chickenMultipliers(diff.p, diff.lanes); const id = rid(); rounds.set(id, { type: 'chicken', userId, bet, p: diff.p, lanes: diff.lanes, mults, lane: 0, alive: true, difficulty }); return { ok: true, roundId: id, lanes: diff.lanes, multipliers: mults, balance: d.balance }; } export function chickenStep(userId, roundId) { const r = rounds.get(roundId); if (!r || r.type !== 'chicken' || r.userId !== userId || !r.alive) return { ok: false, error: 'Runde ungueltig' }; // Crash-Check auf der naechsten Spur if (rand() < r.p) { r.alive = false; rounds.delete(roundId); const bal = settle(userId, 'chicken', r.bet, 0, { crashedAt: r.lane + 1, difficulty: r.difficulty }); return { ok: true, dead: true, lane: r.lane + 1, balance: bal }; } r.lane += 1; const multiplier = r.mults[r.lane - 1]; if (r.lane >= r.lanes) { // Maximum erreicht -> automatischer Cashout const payout = round(r.bet * multiplier); credit(userId, payout); rounds.delete(roundId); const bal = settle(userId, 'chicken', r.bet, payout, { lane: r.lane, multiplier, maxed: true }); return { ok: true, dead: false, maxed: true, lane: r.lane, multiplier, payout, balance: bal }; } return { ok: true, dead: false, lane: r.lane, multiplier, nextMultiplier: r.mults[r.lane] }; } export function chickenCashout(userId, roundId) { const r = rounds.get(roundId); if (!r || r.type !== 'chicken' || r.userId !== userId || !r.alive) return { ok: false, error: 'Runde ungueltig' }; if (r.lane === 0) return { ok: false, error: 'Erst mind. 1 Schritt gehen' }; const multiplier = r.mults[r.lane - 1]; const payout = round(r.bet * multiplier); credit(userId, payout); rounds.delete(roundId); const bal = settle(userId, 'chicken', r.bet, payout, { lane: r.lane, multiplier }); return { ok: true, payout, multiplier, lane: r.lane, balance: bal }; } // ============ 10x10 MINES KARTEN-DECK ============ const GRID = 100; function minesMultiplier(bombs, picks) { let m = 1; for (let i = 0; i < picks; i++) { m *= (GRID - i) / (GRID - bombs - i); } return round(m * HOUSE_EDGE, 4); } export function minesStart(userId, bet, bombs = 10) { bet = Number(bet); bombs = Math.max(1, Math.min(95, Math.floor(Number(bombs)))); const d = debit(userId, bet); if (!d.ok) return d; // Bomben-Positionen geheim ziehen const positions = new Set(); while (positions.size < bombs) positions.add(Math.floor(rand() * GRID)); const id = rid(); rounds.set(id, { type: 'mines', userId, bet, bombs, mines: positions, revealed: new Set(), alive: true }); return { ok: true, roundId: id, bombs, grid: GRID, balance: d.balance, nextMultiplier: minesMultiplier(bombs, 1) }; } export function minesReveal(userId, roundId, index) { const r = rounds.get(roundId); if (!r || r.type !== 'mines' || r.userId !== userId || !r.alive) return { ok: false, error: 'Runde ungueltig' }; index = Math.floor(Number(index)); if (index < 0 || index >= GRID) return { ok: false, error: 'Feld ungueltig' }; if (r.revealed.has(index)) return { ok: false, error: 'Feld schon offen' }; if (r.mines.has(index)) { r.alive = false; rounds.delete(roundId); const bal = settle(userId, 'mines', r.bet, 0, { hitBomb: index, bombs: r.bombs }); return { ok: true, bomb: true, index, mines: [...r.mines], balance: bal }; } r.revealed.add(index); const picks = r.revealed.size; const safeLeft = GRID - r.bombs - picks; const multiplier = minesMultiplier(r.bombs, picks); const next = safeLeft > 0 ? minesMultiplier(r.bombs, picks + 1) : null; if (safeLeft === 0) { // Alle sicheren Felder gefunden -> Auto-Cashout const payout = round(r.bet * multiplier); credit(userId, payout); rounds.delete(roundId); const bal = settle(userId, 'mines', r.bet, payout, { picks, multiplier, cleared: true }); return { ok: true, bomb: false, index, picks, multiplier, payout, cleared: true, balance: bal }; } return { ok: true, bomb: false, index, picks, multiplier, nextMultiplier: next }; } export function minesCashout(userId, roundId) { const r = rounds.get(roundId); if (!r || r.type !== 'mines' || r.userId !== userId || !r.alive) return { ok: false, error: 'Runde ungueltig' }; const picks = r.revealed.size; if (picks === 0) return { ok: false, error: 'Erst mind. 1 Karte aufdecken' }; const multiplier = minesMultiplier(r.bombs, picks); const payout = round(r.bet * multiplier); credit(userId, payout); rounds.delete(roundId); const bal = settle(userId, 'mines', r.bet, payout, { picks, multiplier }); return { ok: true, payout, multiplier, picks, mines: [...r.mines], balance: bal }; } // ============ COINFLIP ============ export function coinflip(userId, bet, choice) { bet = Number(bet); const d = debit(userId, bet); if (!d.ok) return d; const flip = rand() < 0.5 ? 'heads' : 'tails'; const win = flip === choice; const mult = 2 * HOUSE_EDGE; const payout = win ? round(bet * mult) : 0; if (win) credit(userId, payout); const bal = settle(userId, 'coinflip', bet, payout, { flip, choice }); return { ok: true, win, flip, payout, multiplier: win ? mult : 0, balance: bal }; } // ============ DICE (roll under) ============ export function dice(userId, bet, target, direction = 'under') { bet = Number(bet); target = Math.max(2, Math.min(98, Number(target))); const d = debit(userId, bet); if (!d.ok) return d; const roll = round(rand() * 100, 2); const win = direction === 'under' ? roll < target : roll > target; const chance = direction === 'under' ? target : 100 - target; const mult = round((100 / chance) * HOUSE_EDGE, 4); const payout = win ? round(bet * mult) : 0; if (win) credit(userId, payout); const bal = settle(userId, 'dice', bet, payout, { roll, target, direction }); return { ok: true, win, roll, target, direction, multiplier: mult, payout, balance: bal }; } // ============ CRASH ============ // Server-autoritativ: Multiplikator = e^(k*t). Crashpunkt geheim. const CRASH_K = 0.00006; // Wachstum pro ms function genCrashPoint() { const r = rand(); if (r < 0.03) return 1.0; // Instant-Crash 3% // Schwerer Tail, Median ~2x return round(Math.max(1, (1 - 0.01) / (1 - r)), 2); } function crashMultAt(ms) { return round(Math.exp(CRASH_K * ms), 2); } export function crashStart(userId, bet) { bet = Number(bet); const d = debit(userId, bet); if (!d.ok) return d; const id = rid(); const crashPoint = genCrashPoint(); rounds.set(id, { type: 'crash', userId, bet, crashPoint, start: Date.now(), done: false }); return { ok: true, roundId: id, balance: d.balance, k: CRASH_K }; } export function crashCashout(userId, roundId) { const r = rounds.get(roundId); if (!r || r.type !== 'crash' || r.userId !== userId || r.done) return { ok: false, error: 'Runde ungueltig' }; const elapsed = Date.now() - r.start; const current = crashMultAt(elapsed); r.done = true; rounds.delete(roundId); if (current >= r.crashPoint) { const bal = settle(userId, 'crash', r.bet, 0, { crashPoint: r.crashPoint, cashoutAt: current }); return { ok: true, crashed: true, crashPoint: r.crashPoint, balance: bal }; } const payout = round(r.bet * current); credit(userId, payout); const bal = settle(userId, 'crash', r.bet, payout, { multiplier: current, crashPoint: r.crashPoint }); return { ok: true, crashed: false, multiplier: current, payout, balance: bal }; } // Status-Abfrage fuer die Crash-Animation (autoritativ). export function crashStatus(userId, roundId) { const r = rounds.get(roundId); if (!r || r.type !== 'crash' || r.userId !== userId) return { ok: false, error: 'Runde ungueltig' }; const elapsed = Date.now() - r.start; const current = crashMultAt(elapsed); if (current >= r.crashPoint && !r.done) { r.done = true; rounds.delete(roundId); settle(userId, 'crash', r.bet, 0, { crashPoint: r.crashPoint, busted: true }); return { ok: true, crashed: true, crashPoint: r.crashPoint, multiplier: r.crashPoint }; } return { ok: true, crashed: false, multiplier: current }; }