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 @@