Added Dice Game, Fixed Mines & Balance Display on all Gambling Games

This update includes the following changes and improvements:

-  **Mines Game**
  - Added safety guards to prevent the edge-case failures that happend before.
  - Replaced the mine amount selector input with `+` and `–` buttons based on the suggestion.

- 🎲 **Dice Game**
  - Integrated a new Dice game based on [this CodePen implementation](https://codepen.io/oradler/pen/zxxdqKe).
  - Included a sound effect for dice rolls to enhance user interaction. ( No Copyright Issues there )
This commit is contained in:
MD1125 2025-06-15 20:50:55 +02:00
parent 11197d1382
commit 39991cb6c3
12 changed files with 780 additions and 67 deletions

View file

@ -95,11 +95,7 @@
class="text-primary underline hover:cursor-pointer"
onclick={() => (shouldSignIn = !shouldSignIn)}>sign in</button
>
or{' '}
<button
class="text-primary underline hover:cursor-pointer"
onclick={() => (shouldSignIn = !shouldSignIn)}>create an account</button
> to play.
to play.
{/if}
</p>
</header>

View file

@ -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 });
}
};

View file

@ -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
};
});

View file

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

View file

@ -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<number>();
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);

View file

@ -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
</Button>
<Button
variant={activeGame === 'dice' ? 'default' : 'outline'}
onclick={() => (activeGame = 'dice')}
>
Dice
</Button>
</div>
<!-- Game Content -->
@ -87,6 +94,8 @@
<Slots bind:balance onBalanceUpdate={handleBalanceUpdate} />
{:else if activeGame === 'mines'}
<Mines bind:balance onBalanceUpdate={handleBalanceUpdate} />
{:else if activeGame === 'dice'}
<Dice bind:balance onBalanceUpdate={handleBalanceUpdate} />
{/if}
{/if}
</div>