diff --git a/website/src/hooks.server.ts b/website/src/hooks.server.ts index 26a51b1..8a37675 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/components/self/games/Coinflip.svelte b/website/src/lib/components/self/games/Coinflip.svelte index daa940d..e4aac9a 100644 --- a/website/src/lib/components/self/games/Coinflip.svelte +++ b/website/src/lib/components/self/games/Coinflip.svelte @@ -164,6 +164,7 @@ import { formatValue, playSound, showConfetti } from '$lib/utils'; import { volumeSettings } from '$lib/stores/volume-settings'; import { onMount } from 'svelte'; + import { fetchPortfolioSummary } from '$lib/stores/portfolio-data'; interface CoinflipResult { won: boolean; @@ -314,8 +315,18 @@ } } - onMount(() => { + onMount(async () => { volumeSettings.load(); + + try { + const data = await fetchPortfolioSummary(); + if (data) { + balance = data.baseCurrencyBalance; + onBalanceUpdate?.(data.baseCurrencyBalance); + } + } catch (error) { + console.error('Failed to fetch balance:', error); + } }); diff --git a/website/src/lib/components/self/games/Dice.svelte b/website/src/lib/components/self/games/Dice.svelte new file mode 100644 index 0000000..ea4430c --- /dev/null +++ b/website/src/lib/components/self/games/Dice.svelte @@ -0,0 +1,419 @@ + + + + + Dice + Choose a number and roll the dice to win 3x your bet! + + +
+
+
+

Balance

+

{formatValue(balance)}

+
+ +
+
+
+ {#each Array(6) as _, i} +
+
+ {#each Array(i + 1) as _} +
+ {/each} +
+
+ {/each} +
+
+
+ +
+ {#if lastResult && !isRolling} +
+ {#if lastResult.won} +

WIN

+

+ Won {formatValue(lastResult.payout)} on {lastResult.result} +

+ {:else} +

LOSS

+

+ Lost {formatValue(lastResult.amountWagered)} on {lastResult.result} +

+ {/if} +
+ {/if} +
+
+ +
+
+
Choose Number
+
+ {#each Array(6) as _, i} + + {/each} +
+
+ +
+ + +

+ Max bet: {MAX_BET_AMOUNT.toLocaleString()} +

+
+ +
+
+ + + + +
+
+ + +
+
+
+
+ + diff --git a/website/src/lib/components/self/games/Mines.svelte b/website/src/lib/components/self/games/Mines.svelte new file mode 100644 index 0000000..7348ca8 --- /dev/null +++ b/website/src/lib/components/self/games/Mines.svelte @@ -0,0 +1,537 @@ + + + + + Mines + + Navigate through the minefield and cash out before hitting a mine! + + + +
+ +
+ +
+

Balance

+

{formatValue(balance)}

+
+ + +
= 7}> + {#each Array(TOTAL_TILES) as _, index} + + + {/each} +
+
+ +
+
+ +
+ + { + const target = e.target as HTMLInputElement | null; + const val = Math.max( + MIN_MINES, + Math.min(24, parseInt(target?.value ?? '') || MIN_MINES) + ); + mineCount = val; + }} + disabled={isPlaying} + class="w-12 text-center [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" + /> + +
+

+ You will get + + {calculateRawMultiplier(isPlaying ? revealedTiles.length + 1 : 1, mineCount).toFixed( + 2 + )}x + + per tile, probability of winning: + + {calculateProbability(isPlaying ? 1 : 1, mineCount)}% + +

+ + Note: Maximum payout per game is capped at $2,000,000. + +
+
+ + +

+ Max bet: {MAX_BET_AMOUNT.toLocaleString()} +

+
+
+
+ + + + +
+
+
+ {#if !isPlaying} + + {:else} + {#if hasRevealedTile} +
+
+
+ Auto Cashout in {Math.ceil(AUTO_CASHOUT_TIME - autoCashoutTimer)}s +
+
+
= 7} + style="width: {autoCashoutProgress}%" + >
+
+
+
+ {/if} + + + {#if hasRevealedTile} +
+
+ Current Profit: + + +{formatValue(betAmount * (currentMultiplier - 1))} + +
+
+ Next Tile: + + +{formatValue( + betAmount * (calculateRawMultiplier(revealedTiles.length + 1, mineCount) - 1) + )} + +
+
+ Current Multiplier: + {currentMultiplier.toFixed(2)}x +
+
+ {/if} + {/if} +
+
+
+
+
+ + diff --git a/website/src/lib/components/self/games/Slots.svelte b/website/src/lib/components/self/games/Slots.svelte index 6ed0c6c..240339e 100644 --- a/website/src/lib/components/self/games/Slots.svelte +++ b/website/src/lib/components/self/games/Slots.svelte @@ -13,6 +13,7 @@ import { formatValue, playSound, showConfetti, showSchoolPrideCannons } from '$lib/utils'; import { volumeSettings } from '$lib/stores/volume-settings'; import { onMount } from 'svelte'; + import { fetchPortfolioSummary } from '$lib/stores/portfolio-data'; interface SlotsResult { won: boolean; @@ -211,8 +212,19 @@ } }); - onMount(() => { + // Dynmaically fetch the correct balance. + onMount(async () => { volumeSettings.load(); + + try { + const data = await fetchPortfolioSummary(); + if (data) { + balance = data.baseCurrencyBalance; + onBalanceUpdate?.(data.baseCurrencyBalance); + } + } catch (error) { + console.error('Failed to fetch balance:', error); + } }); diff --git a/website/src/lib/server/games/mines.ts b/website/src/lib/server/games/mines.ts new file mode 100644 index 0000000..1b64eda --- /dev/null +++ b/website/src/lib/server/games/mines.ts @@ -0,0 +1,163 @@ +import { db } from '$lib/server/db'; +import { user } from '$lib/server/db/schema'; +import { eq } from 'drizzle-orm'; +import { redis } from '$lib/server/redis'; + +interface MinesSession { + sessionToken: string; + betAmount: number; + mineCount: number; + minePositions: number[]; + revealedTiles: number[]; + startTime: number; + currentMultiplier: number; + status: 'active' | 'won' | 'lost'; + lastActivity: number; + userId: number; +} + +const MINES_SESSION_PREFIX = 'mines:session:'; +export const getSessionKey = (token: string) => `${MINES_SESSION_PREFIX}${token}`; + +// --- Mines cleanup logic for scheduler --- +export async function minesCleanupInactiveGames() { + const now = Date.now(); + 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; + const game = JSON.parse(sessionRaw) as MinesSession; + if (now - game.lastActivity > 5 * 60 * 1000) { + if (game.revealedTiles.length === 0) { + try { + const [userData] = await db + .select({ baseCurrencyBalance: user.baseCurrencyBalance }) + .from(user) + .where(eq(user.id, game.userId)) + .for('update') + .limit(1); + + const currentBalance = Number(userData.baseCurrencyBalance); + const newBalance = Math.round((currentBalance + game.betAmount) * 100000000) / 100000000; + + await db + .update(user) + .set({ + baseCurrencyBalance: newBalance.toFixed(8), + updatedAt: new Date() + }) + .where(eq(user.id, game.userId)); + } catch (error) { + console.error(`Failed to refund inactive game ${game.sessionToken}:`, error); + } + } + await redis.del(getSessionKey(game.sessionToken)); + } + } +} + +export async function minesAutoCashout() { + const now = Date.now(); + 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; + const game = JSON.parse(sessionRaw) as MinesSession; + + if ( + game.status === 'active' && + game.revealedTiles.length > 0 && + now - game.lastActivity > 20000 && + !game.revealedTiles.some(idx => game.minePositions.includes(idx)) + ) { + try { + const [userData] = await db + .select({ baseCurrencyBalance: user.baseCurrencyBalance }) + .from(user) + .where(eq(user.id, game.userId)) + .for('update') + .limit(1); + + const currentBalance = Number(userData.baseCurrencyBalance); + const payout = game.betAmount * game.currentMultiplier; + const roundedPayout = Math.round(payout * 100000000) / 100000000; + const newBalance = Math.round((currentBalance + roundedPayout) * 100000000) / 100000000; + + await db + .update(user) + .set({ + baseCurrencyBalance: newBalance.toFixed(8), + updatedAt: new Date() + }) + .where(eq(user.id, game.userId)); + + await redis.del(getSessionKey(game.sessionToken)); + } catch (error) { + console.error(`Failed to auto cashout game ${game.sessionToken}:`, error); + } + } + } +} + +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 baseMultiplier = (1.4 + Math.pow(picks, 0.45)) * mineFactor; + + if (bet > HIGH_BET_THRESHOLD) { + const betRatio = Math.pow(Math.min(1, (bet - HIGH_BET_THRESHOLD) / (MAX_PAYOUT - HIGH_BET_THRESHOLD)), 1); + + // 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 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; +}; + +export function calculateMultiplier(picks: number, mines: number, betAmount: number): number { + const TOTAL_TILES = 25; + const HOUSE_EDGE = 0.05; + + // Calculate probability of winning based on picks and mines + let probability = 1; + for (let i = 0; i < picks; i++) { + probability *= (TOTAL_TILES - mines - i) / (TOTAL_TILES - i); + } + + if (probability <= 0) return 1.0; + + // 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); + const effectiveMultiplier = cappedPayout / betAmount; + + return Math.max(1.0, Number(effectiveMultiplier.toFixed(2))); +} + + diff --git a/website/src/routes/+page.svelte b/website/src/routes/+page.svelte index e51493d..0fee172 100644 --- a/website/src/routes/+page.svelte +++ b/website/src/routes/+page.svelte @@ -95,11 +95,7 @@ class="text-primary underline hover:cursor-pointer" onclick={() => (shouldSignIn = !shouldSignIn)}>sign in - or{' '} - to play. + to play. {/if}

diff --git a/website/src/routes/api/gambling/dice/+server.ts b/website/src/routes/api/gambling/dice/+server.ts new file mode 100644 index 0000000..d023cf6 --- /dev/null +++ b/website/src/routes/api/gambling/dice/+server.ts @@ -0,0 +1,87 @@ +import { auth } from '$lib/auth'; +import { error, json } from '@sveltejs/kit'; +import { db } from '$lib/server/db'; +import { user } from '$lib/server/db/schema'; +import { eq } from 'drizzle-orm'; +import { randomBytes } from 'crypto'; +import type { RequestHandler } from './$types'; + +interface DiceRequest { + selectedNumber: number; + amount: number; +} + +export const POST: RequestHandler = async ({ request }) => { + const session = await auth.api.getSession({ + headers: request.headers + }); + + if (!session?.user) { + throw error(401, 'Not authenticated'); + } + + try { + const { selectedNumber, amount }: DiceRequest = await request.json(); + + if (!selectedNumber || selectedNumber < 1 || selectedNumber > 6 || !Number.isInteger(selectedNumber)) { + return json({ error: 'Invalid number selection' }, { status: 400 }); + } + + if (!amount || amount <= 0 || !Number.isFinite(amount)) { + return json({ error: 'Invalid bet amount' }, { status: 400 }); + } + + if (amount > 1000000) { + return json({ error: 'Bet amount too large' }, { status: 400 }); + } + + const userId = Number(session.user.id); + + const result = await db.transaction(async (tx) => { + const [userData] = await tx + .select({ baseCurrencyBalance: user.baseCurrencyBalance }) + .from(user) + .where(eq(user.id, userId)) + .for('update') + .limit(1); + + const currentBalance = Number(userData.baseCurrencyBalance); + + const roundedAmount = Math.round(amount * 100000000) / 100000000; + const roundedBalance = Math.round(currentBalance * 100000000) / 100000000; + + if (roundedAmount > roundedBalance) { + throw new Error(`Insufficient funds. You need *${roundedAmount.toFixed(2)} but only have *${roundedBalance.toFixed(2)}`); + } + + const gameResult = Math.floor(randomBytes(1)[0] / 42.67) + 1; // This gives us a number between 1-6 + const won = gameResult === selectedNumber; + + const multiplier = 3; + const payout = won ? roundedAmount * multiplier : 0; + const newBalance = roundedBalance - roundedAmount + payout; + + await tx + .update(user) + .set({ + baseCurrencyBalance: newBalance.toFixed(8), + updatedAt: new Date() + }) + .where(eq(user.id, userId)); + + return { + won, + result: gameResult, + newBalance, + payout, + amountWagered: roundedAmount + }; + }); + + return json(result); + } catch (e) { + console.error('Dice API error:', e); + const errorMessage = e instanceof Error ? e.message : 'Internal server error'; + return json({ error: errorMessage }, { status: 400 }); + } +}; diff --git a/website/src/routes/api/gambling/mines/cashout/+server.ts b/website/src/routes/api/gambling/mines/cashout/+server.ts new file mode 100644 index 0000000..dd9b7a7 --- /dev/null +++ b/website/src/routes/api/gambling/mines/cashout/+server.ts @@ -0,0 +1,76 @@ +import { auth } from '$lib/auth'; +import { error, json } from '@sveltejs/kit'; +import { db } from '$lib/server/db'; +import { user } from '$lib/server/db/schema'; +import { eq } from 'drizzle-orm'; +import { redis } from '$lib/server/redis'; +import { getSessionKey } from '$lib/server/games/mines'; +import type { RequestHandler } from './$types'; + +export const POST: RequestHandler = async ({ request }) => { + const session = await auth.api.getSession({ + headers: request.headers + }); + + if (!session?.user) { + throw error(401, 'Not authenticated'); + } + + try { + const { sessionToken } = await request.json(); + const sessionRaw = await redis.get(getSessionKey(sessionToken)); + const game = sessionRaw ? JSON.parse(sessionRaw) : null; + const userId = Number(session.user.id); + + if (!game) { + return json({ error: 'Invalid session' }, { status: 400 }); + } + + const result = await db.transaction(async (tx) => { + const [userData] = await tx + .select({ baseCurrencyBalance: user.baseCurrencyBalance }) + .from(user) + .where(eq(user.id, userId)) + .for('update') + .limit(1); + + const currentBalance = Number(userData.baseCurrencyBalance); + let payout: number; + let newBalance: number; + + // If no tiles revealed, treat as abort and return full bet. + if (game.revealedTiles.length === 0) { + payout = game.betAmount; + newBalance = Math.round((currentBalance + payout) * 100000000) / 100000000; + } else { + payout = game.betAmount * game.currentMultiplier; + const roundedPayout = Math.round(payout * 100000000) / 100000000; + newBalance = Math.round((currentBalance + roundedPayout) * 100000000) / 100000000; + } + + await tx + .update(user) + .set({ + baseCurrencyBalance: newBalance.toFixed(8), + updatedAt: new Date() + }) + .where(eq(user.id, userId)); + + await redis.del(getSessionKey(sessionToken)); + + return { + newBalance, + payout, + amountWagered: game.betAmount, + isAbort: game.revealedTiles.length === 0, + minePositions: game.minePositions + }; + }); + + return json(result); + } catch (e) { + console.error('Mines cashout error:', e); + const errorMessage = e instanceof Error ? e.message : 'Internal server error'; + return json({ error: errorMessage }, { status: 400 }); + } +}; \ No newline at end of file diff --git a/website/src/routes/api/gambling/mines/reveal/+server.ts b/website/src/routes/api/gambling/mines/reveal/+server.ts new file mode 100644 index 0000000..e36744d --- /dev/null +++ b/website/src/routes/api/gambling/mines/reveal/+server.ts @@ -0,0 +1,127 @@ +import { auth } from '$lib/auth'; +import { error, json } from '@sveltejs/kit'; +import { calculateMultiplier } from '$lib/server/games/mines'; +import type { RequestHandler } from './$types'; +import { db } from '$lib/server/db'; +import { user } from '$lib/server/db/schema'; +import { eq } from 'drizzle-orm'; +import { redis, } from '$lib/server/redis'; +import { getSessionKey } from '$lib/server/games/mines'; + +export const POST: RequestHandler = async ({ request }) => { + const session = await auth.api.getSession({ + headers: request.headers + }); + + if (!session?.user) { + throw error(401, 'Not authenticated'); + } + + 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; + + if (!game) { + return json({ error: 'Invalid session' }, { status: 400 }); + } + + if (game.revealedTiles.includes(tileIndex)) { + return json({ error: 'Tile already revealed' }, { status: 400 }); + } + + game.lastActivity = Date.now(); + + if (game.minePositions.includes(tileIndex)) { + game.status = 'lost'; + const minePositions = game.minePositions; + + const userId = Number(session.user.id); + const [userData] = await db + .select({ baseCurrencyBalance: user.baseCurrencyBalance }) + .from(user) + .where(eq(user.id, userId)) + .for('update') + .limit(1); + + const currentBalance = Number(userData.baseCurrencyBalance); + + await db + .update(user) + .set({ + baseCurrencyBalance: currentBalance.toFixed(8), + updatedAt: new Date() + }) + .where(eq(user.id, userId)); + + await redis.del(getSessionKey(sessionToken)); + + return json({ + hitMine: true, + minePositions, + newBalance: currentBalance, + status: 'lost', + amountWagered: game.betAmount + }); + } + + // Safe tile + game.revealedTiles.push(tileIndex); + game.currentMultiplier = calculateMultiplier( + game.revealedTiles.length, + game.mineCount, + game.betAmount + ); + + if (game.revealedTiles.length === 25 - game.mineCount) { + game.status = 'won'; + const userId = Number(session.user.id); + const [userData] = await db + .select({ baseCurrencyBalance: user.baseCurrencyBalance }) + .from(user) + .where(eq(user.id, userId)) + .for('update') + .limit(1); + + const currentBalance = Number(userData.baseCurrencyBalance); + const payout = game.betAmount * game.currentMultiplier; + const roundedPayout = Math.round(payout * 100000000) / 100000000; + const newBalance = Math.round((currentBalance + roundedPayout) * 100000000) / 100000000; + + await db + .update(user) + .set({ + baseCurrencyBalance: newBalance.toFixed(8), + updatedAt: new Date() + }) + .where(eq(user.id, userId)); + + await redis.del(getSessionKey(sessionToken)); + + return json({ + hitMine: false, + currentMultiplier: game.currentMultiplier, + status: 'won', + newBalance, + payout + }); + } + + await redis.set(getSessionKey(sessionToken), JSON.stringify(game)); + + return json({ + hitMine: false, + currentMultiplier: game.currentMultiplier, + status: game.status + }); + } catch (e) { + console.error('Mines reveal error:', e); + const errorMessage = e instanceof Error ? e.message : 'Internal server error'; + return json({ error: errorMessage }, { status: 400 }); + } +}; \ No newline at end of file diff --git a/website/src/routes/api/gambling/mines/start/+server.ts b/website/src/routes/api/gambling/mines/start/+server.ts new file mode 100644 index 0000000..b7ab5f6 --- /dev/null +++ b/website/src/routes/api/gambling/mines/start/+server.ts @@ -0,0 +1,100 @@ +import { auth } from '$lib/auth'; +import { error, json } from '@sveltejs/kit'; +import { db } from '$lib/server/db'; +import { user } from '$lib/server/db/schema'; +import { eq } from 'drizzle-orm'; +import { redis } from '$lib/server/redis'; +import { getSessionKey } from '$lib/server/games/mines'; +import type { RequestHandler } from './$types'; + +export const POST: RequestHandler = async ({ request }) => { + const session = await auth.api.getSession({ + headers: request.headers + }); + + if (!session?.user) { + throw error(401, 'Not authenticated'); + } + + try { + const { betAmount, mineCount } = await request.json(); + const userId = Number(session.user.id); + + if (!betAmount || betAmount <= 0 || !mineCount || mineCount < 3 || mineCount > 24) { + return json({ error: 'Invalid bet amount or mine count' }, { status: 400 }); + } + + if (betAmount > 1000000) { + return json({ error: 'Bet amount too large' }, { status: 400 }); + } + + const result = await db.transaction(async (tx) => { + const [userData] = await tx + .select({ baseCurrencyBalance: user.baseCurrencyBalance }) + .from(user) + .where(eq(user.id, userId)) + .for('update') + .limit(1); + + const currentBalance = Number(userData.baseCurrencyBalance); + const roundedAmount = Math.round(betAmount * 100000000) / 100000000; + const roundedBalance = Math.round(currentBalance * 100000000) / 100000000; + + if (roundedAmount > roundedBalance) { + throw new Error(`Insufficient funds. You need *${roundedAmount.toFixed(2)} but only have *${roundedBalance.toFixed(2)}`); + } + + // Generate mine positions + const positions = new Set(); + while (positions.size < mineCount) { + positions.add(Math.floor(Math.random() * 25)); + } + + // transaction token for authentication stuff + const randomBytes = new Uint8Array(8); + crypto.getRandomValues(randomBytes); + const sessionToken = Array.from(randomBytes) + .map(b => b.toString(16).padStart(2, '0')) + .join(''); + + const now = Date.now(); + const newBalance = roundedBalance - roundedAmount; + + await redis.set( + getSessionKey(sessionToken), + JSON.stringify({ + sessionToken, + betAmount: roundedAmount, + mineCount, + minePositions: Array.from(positions), + revealedTiles: [], + startTime: now, + lastActivity: now, + currentMultiplier: 1, + status: 'active', + userId + }) + ); + + // Update user balance + await tx + .update(user) + .set({ + baseCurrencyBalance: newBalance.toFixed(8), + updatedAt: new Date() + }) + .where(eq(user.id, userId)); + + return { + sessionToken, + newBalance + }; + }); + + return json(result); + } catch (e) { + console.error('Mines start error:', e); + const errorMessage = e instanceof Error ? e.message : 'Internal server error'; + return json({ error: errorMessage }, { status: 400 }); + } +}; \ No newline at end of file diff --git a/website/src/routes/gambling/+page.svelte b/website/src/routes/gambling/+page.svelte index 4df7bde..996aeb4 100644 --- a/website/src/routes/gambling/+page.svelte +++ b/website/src/routes/gambling/+page.svelte @@ -1,6 +1,7 @@