-
-
-
-
-
-
+
+
+
+
+
+
{mineCount}
+
+
Mines
+
+
+
@@ -611,4 +638,4 @@
height: 32px;
object-fit: contain;
}
-
+
\ No newline at end of file
diff --git a/website/src/lib/components/self/games/Slots.svelte b/website/src/lib/components/self/games/Slots.svelte
index 6ed0c6c..d935979 100644
--- a/website/src/lib/components/self/games/Slots.svelte
+++ b/website/src/lib/components/self/games/Slots.svelte
@@ -211,8 +211,21 @@
}
});
- onMount(() => {
+ // Dynmaically fetch the correct balance.
+ onMount(async () => {
volumeSettings.load();
+
+ try {
+ const response = await fetch('/api/portfolio/summary');
+ if (!response.ok) {
+ throw new Error('Failed to fetch portfolio summary');
+ }
+ const data = await response.json();
+ 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
index 2c95f24..766e10d 100644
--- a/website/src/lib/server/games/mines.ts
+++ b/website/src/lib/server/games/mines.ts
@@ -19,17 +19,13 @@ interface MinesSession {
export const activeGames = new Map();
-// Clean up old games every minute.
+// Clean up old games every minute. (5 Minute system)
setInterval(async () => {
const now = Date.now();
for (const [token, game] of activeGames.entries()) {
- // Delete games older than 5 minutes that are still there for some reason.
if (now - game.lastActivity > 5 * 60 * 1000) {
- // If no tiles were revealed, refund the bet
if (game.revealedTiles.length === 0) {
- try {
- console.log(`Processing refund for inactive Mines game ${token} (User: ${game.userId}, Bet: ${game.betAmount})`);
-
+ try {
const [userData] = await db
.select({ baseCurrencyBalance: user.baseCurrencyBalance })
.from(user)
@@ -48,29 +44,74 @@ setInterval(async () => {
})
.where(eq(user.id, game.userId));
- console.log(`Successfully refunded ${game.betAmount} to user ${game.userId}. New balance: ${newBalance}`);
} catch (error) {
console.error(`Failed to refund inactive game ${token}:`, error);
}
- } else {
- console.log(`Cleaning up inactive game ${token} (User: ${game.userId}) - No refund needed as tiles were revealed`);
}
activeGames.delete(token);
}
}
}, 60000);
+setInterval(async () => {
+ const now = Date.now();
+ for (const [token, game] of activeGames.entries()) {
+ if (game.status === 'active' && game.revealedTiles.length > 0 && now - game.lastActivity > 20000) {
+ 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));
+
+ activeGames.delete(token);
+ } catch (error) {
+ console.error(`Failed to auto cashout game ${token}:`, error);
+ }
+ }
+ }
+}, 15000);
+
// Rig the game...
-const getMaxPayout = (bet: number, picks: number): number => {
- const absoluteCap = 5_000_000; // never pay above this. Yeah, its rigged. Live with that :)
- const baseCap = 1.4; // 1.4x min multiplier, increase to, well, increase payouts
- const growthRate = 0.45; // cap curve sensitivity
-
- // Cap increases with number of successful reveals
- const effectiveMultiplierCap = baseCap + Math.pow(picks, growthRate);
- const payoutCap = bet * effectiveMultiplierCap;
-
- return Math.min(payoutCap, absoluteCap);
+const getMaxPayout = (bet: number, picks: number, mines: number): number => {
+ const MAX_PAYOUT = 2_000_000; // Maximum payout cap of 2 million to not make linker too rich
+ const HIGH_BET_THRESHOLD = 50_000;
+
+ const mineFactor = 1 + (mines / 25);
+ const baseMultiplier = (1.4 + Math.pow(picks, 0.45)) * mineFactor;
+
+ // For high bets, we stop linker from getting richer ¯\_(ツ)_/¯
+ 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;
};
@@ -78,6 +119,7 @@ export function calculateMultiplier(picks: number, mines: number, betAmount: num
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);
@@ -85,14 +127,15 @@ export function calculateMultiplier(picks: number, mines: number, betAmount: num
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);
+ const maxPayout = getMaxPayout(betAmount, picks, mines);
const cappedPayout = Math.min(rawPayout, maxPayout);
const effectiveMultiplier = cappedPayout / betAmount;
- return Math.max(1.0, effectiveMultiplier);
+ 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
index a91bb68..773e9ad 100644
--- a/website/src/routes/api/gambling/mines/cashout/+server.ts
+++ b/website/src/routes/api/gambling/mines/cashout/+server.ts
@@ -47,6 +47,7 @@ export const POST: RequestHandler = async ({ request }) => {
newBalance = Math.round((currentBalance + roundedPayout) * 100000000) / 100000000;
}
+
await tx
.update(user)
.set({
@@ -55,13 +56,15 @@ export const POST: RequestHandler = async ({ request }) => {
})
.where(eq(user.id, userId));
+
activeGames.delete(sessionToken);
return {
newBalance,
payout,
amountWagered: game.betAmount,
- isAbort: game.revealedTiles.length === 0
+ isAbort: game.revealedTiles.length === 0,
+ minePositions: game.minePositions
};
});
diff --git a/website/src/routes/api/gambling/mines/reveal/+server.ts b/website/src/routes/api/gambling/mines/reveal/+server.ts
index 75f95c7..6a711ec 100644
--- a/website/src/routes/api/gambling/mines/reveal/+server.ts
+++ b/website/src/routes/api/gambling/mines/reveal/+server.ts
@@ -27,35 +27,46 @@ export const POST: RequestHandler = async ({ request }) => {
return json({ error: 'Tile already revealed' }, { status: 400 });
}
- // Update last activity time
game.lastActivity = Date.now();
- // Check if hit mine
if (game.minePositions.includes(tileIndex)) {
game.status = 'lost';
const minePositions = game.minePositions;
- // Fetch user balance to return after loss
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));
activeGames.delete(sessionToken);
+
return json({
hitMine: true,
minePositions,
- newBalance: Number(userData.baseCurrencyBalance),
- status: 'lost'
+ newBalance: currentBalance,
+ status: 'lost',
+ amountWagered: game.betAmount
});
}
- // Safe tile
+ // Safe tile (Yipeee)
game.revealedTiles.push(tileIndex);
game.currentMultiplier = calculateMultiplier(
game.revealedTiles.length,
@@ -63,9 +74,38 @@ export const POST: RequestHandler = async ({ request }) => {
game.betAmount
);
- // Check if all safe tiles are revealed. Crazy when you get this :)
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));
+
+ activeGames.delete(sessionToken);
+
+ return json({
+ hitMine: false,
+ currentMultiplier: game.currentMultiplier,
+ status: 'won',
+ newBalance,
+ payout
+ });
}
return json({
diff --git a/website/src/routes/api/gambling/mines/start/+server.ts b/website/src/routes/api/gambling/mines/start/+server.ts
index 34e25d9..cff9ab4 100644
--- a/website/src/routes/api/gambling/mines/start/+server.ts
+++ b/website/src/routes/api/gambling/mines/start/+server.ts
@@ -43,6 +43,7 @@ export const POST: RequestHandler = async ({ request }) => {
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) {
@@ -52,11 +53,8 @@ export const POST: RequestHandler = async ({ request }) => {
for (let i = 0; i < 25; i++) {
if (!positions.has(i)) safePositions.push(i);
}
- console.log(positions)
- console.log('Safe positions:', safePositions);
-
- // transaction token for authentication
+ // transaction token for authentication stuff
const randomBytes = new Uint8Array(8);
crypto.getRandomValues(randomBytes);
const sessionToken = Array.from(randomBytes)
@@ -64,6 +62,7 @@ export const POST: RequestHandler = async ({ request }) => {
.join('');
const now = Date.now();
+ const newBalance = roundedBalance - roundedAmount;
// Create session
activeGames.set(sessionToken, {
@@ -79,16 +78,20 @@ export const POST: RequestHandler = async ({ request }) => {
userId
});
- // Hold bet amount on server to prevent the user from like sending it to another account and farming money without a risk
+ // Update user balance
await tx
.update(user)
.set({
- baseCurrencyBalance: (roundedBalance - roundedAmount).toFixed(8),
+ baseCurrencyBalance: newBalance.toFixed(8),
updatedAt: new Date()
})
.where(eq(user.id, userId));
- return { sessionToken };
+
+ return {
+ sessionToken,
+ newBalance
+ };
});
return json(result);
diff --git a/website/src/routes/gambling/+page.svelte b/website/src/routes/gambling/+page.svelte
index 27b7d0c..996aeb4 100644
--- a/website/src/routes/gambling/+page.svelte
+++ b/website/src/routes/gambling/+page.svelte
@@ -9,6 +9,7 @@
import SignInConfirmDialog from '$lib/components/self/SignInConfirmDialog.svelte';
import { Button } from '$lib/components/ui/button';
import SEO from '$lib/components/self/SEO.svelte';
+ import Dice from '$lib/components/self/games/Dice.svelte'
let shouldSignIn = $state(false);
let balance = $state(0);
@@ -78,6 +79,12 @@
>
Mines
+
@@ -87,6 +94,8 @@
{:else if activeGame === 'mines'}
+ {:else if activeGame === 'dice'}
+
{/if}
{/if}
diff --git a/website/static/sound/dice.mp3 b/website/static/sound/dice.mp3
new file mode 100644
index 0000000000000000000000000000000000000000..e73d79d6d3925eb2562926cd4644a3892a4883a5
GIT binary patch
literal 44544
zcmZ^qX