From a352314d42aace29ca5ae9d7dfbfe7d3af1f23ea Mon Sep 17 00:00:00 2001 From: Face <69168154+face-hh@users.noreply.github.com> Date: Tue, 24 Jun 2025 13:10:36 +0300 Subject: [PATCH] fix input limits, use redis SCAN, move interval to hooks --- website/src/hooks.server.ts | 7 ++++ website/src/lib/server/games/mines.ts | 42 ++++++++++++------- .../api/gambling/mines/reveal/+server.ts | 5 +++ .../api/gambling/mines/start/+server.ts | 2 +- 4 files changed, 40 insertions(+), 16 deletions(-) diff --git a/website/src/hooks.server.ts b/website/src/hooks.server.ts index abd3d72..9eb55c0 100644 --- a/website/src/hooks.server.ts +++ b/website/src/hooks.server.ts @@ -7,6 +7,7 @@ import { redirect, type Handle } from '@sveltejs/kit'; import { db } from '$lib/server/db'; import { user } from '$lib/server/db/schema'; import { eq } from 'drizzle-orm'; +import { minesCleanupInactiveGames, minesAutoCashout } from '$lib/server/games/mines'; async function initializeScheduler() { if (building) return; @@ -49,10 +50,16 @@ async function initializeScheduler() { processAccountDeletions().catch(console.error); }, 5 * 60 * 1000); + const minesCleanupInterval = setInterval(() => { + minesCleanupInactiveGames().catch(console.error); + minesAutoCashout().catch(console.error); + }, 60 * 1000); + // Cleanup on process exit const cleanup = async () => { clearInterval(renewInterval); clearInterval(schedulerInterval); + clearInterval(minesCleanupInterval); const currentValue = await redis.get(lockKey); if (currentValue === lockValue) { await redis.del(lockKey); diff --git a/website/src/lib/server/games/mines.ts b/website/src/lib/server/games/mines.ts index 6b79b3b..1b64eda 100644 --- a/website/src/lib/server/games/mines.ts +++ b/website/src/lib/server/games/mines.ts @@ -19,10 +19,16 @@ interface MinesSession { const MINES_SESSION_PREFIX = 'mines:session:'; export const getSessionKey = (token: string) => `${MINES_SESSION_PREFIX}${token}`; -// Clean up old games every minute. (5 Minute system) -setInterval(async () => { +// --- Mines cleanup logic for scheduler --- +export async function minesCleanupInactiveGames() { const now = Date.now(); - const keys = await redis.keys(`${MINES_SESSION_PREFIX}*`); + const keys: string[] = []; + let cursor = '0'; + do { + const scanResult = await redis.scan(cursor, { MATCH: `${MINES_SESSION_PREFIX}*` }); + cursor = scanResult.cursor; + keys.push(...scanResult.keys); + } while (cursor !== '0'); for (const key of keys) { const sessionRaw = await redis.get(key); if (!sessionRaw) continue; @@ -54,11 +60,17 @@ setInterval(async () => { await redis.del(getSessionKey(game.sessionToken)); } } -}, 60000); +} -setInterval(async () => { +export async function minesAutoCashout() { const now = Date.now(); - const keys = await redis.keys(`${MINES_SESSION_PREFIX}*`); + const keys: string[] = []; + let cursor = '0'; + do { + const scanResult = await redis.scan(cursor, { MATCH: `${MINES_SESSION_PREFIX}*` }); + cursor = scanResult.cursor; + keys.push(...scanResult.keys); + } while (cursor !== '0'); for (const key of keys) { const sessionRaw = await redis.get(key); if (!sessionRaw) continue; @@ -97,13 +109,13 @@ setInterval(async () => { } } } -}, 15000); +} const getMaxPayout = (bet: number, picks: number, mines: number): number => { const MAX_PAYOUT = 2_000_000; - const HIGH_BET_THRESHOLD = 50_000; - - const mineFactor = 1 + (mines / 25); + const HIGH_BET_THRESHOLD = 50_000; + + const mineFactor = 1 + (mines / 25); const baseMultiplier = (1.4 + Math.pow(picks, 0.45)) * mineFactor; if (bet > HIGH_BET_THRESHOLD) { @@ -112,15 +124,15 @@ const getMaxPayout = (bet: number, picks: number, mines: number): number => { // Direct cap on multiplier for high bets const maxAllowedMultiplier = 1.05 + (picks * 0.1); const highBetMultiplier = Math.min(baseMultiplier, maxAllowedMultiplier) * (1 - (bet / MAX_PAYOUT) * 0.9); - const betSizeFactor = Math.max(0.1, 1 - (bet / MAX_PAYOUT) * 0.9); + const betSizeFactor = Math.max(0.1, 1 - (bet / MAX_PAYOUT) * 0.9); const minMultiplier = (1.1 + (picks * 0.15 * betSizeFactor)) * mineFactor; - + const reducedMultiplier = highBetMultiplier - ((highBetMultiplier - minMultiplier) * betRatio); const payout = Math.min(bet * reducedMultiplier, MAX_PAYOUT); - + return payout; } - + const payout = Math.min(bet * baseMultiplier, MAX_PAYOUT); return payout; }; @@ -139,7 +151,7 @@ export function calculateMultiplier(picks: number, mines: number, betAmount: num // Calculate fair multiplier based on probability and house edge const fairMultiplier = (1 / probability) * (1 - HOUSE_EDGE); - + const rawPayout = fairMultiplier * betAmount; const maxPayout = getMaxPayout(betAmount, picks, mines); const cappedPayout = Math.min(rawPayout, maxPayout); diff --git a/website/src/routes/api/gambling/mines/reveal/+server.ts b/website/src/routes/api/gambling/mines/reveal/+server.ts index 4e1cb59..e36744d 100644 --- a/website/src/routes/api/gambling/mines/reveal/+server.ts +++ b/website/src/routes/api/gambling/mines/reveal/+server.ts @@ -19,6 +19,11 @@ export const POST: RequestHandler = async ({ request }) => { try { const { sessionToken, tileIndex } = await request.json(); + + if (!Number.isInteger(tileIndex) || tileIndex < 0 || tileIndex > 24) { + return json({ error: 'Invalid tileIndex' }, { status: 400 }); + } + const sessionRaw = await redis.get(getSessionKey(sessionToken)); const game = sessionRaw ? JSON.parse(sessionRaw) : null; diff --git a/website/src/routes/api/gambling/mines/start/+server.ts b/website/src/routes/api/gambling/mines/start/+server.ts index 92b8de3..b7ab5f6 100644 --- a/website/src/routes/api/gambling/mines/start/+server.ts +++ b/website/src/routes/api/gambling/mines/start/+server.ts @@ -20,7 +20,7 @@ export const POST: RequestHandler = async ({ request }) => { const { betAmount, mineCount } = await request.json(); const userId = Number(session.user.id); - if (!betAmount || !mineCount || mineCount < 3 || mineCount > 24) { + if (!betAmount || betAmount <= 0 || !mineCount || mineCount < 3 || mineCount > 24) { return json({ error: 'Invalid bet amount or mine count' }, { status: 400 }); }