feat: gambling (coinflip & slots)

This commit is contained in:
Face 2025-05-29 14:12:27 +03:00
parent 543a5a951c
commit 1cae171748
38 changed files with 5631 additions and 1 deletions

View file

@ -0,0 +1,84 @@
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 CoinflipRequest {
side: 'heads' | 'tails';
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 { side, amount }: CoinflipRequest = await request.json();
if (!['heads', 'tails'].includes(side)) {
return json({ error: 'Invalid side' }, { 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);
if (amount > currentBalance) {
throw new Error(`Insufficient funds. You need *${amount.toFixed(2)} but only have *${currentBalance.toFixed(2)}`);
}
const gameResult: 'heads' | 'tails' = randomBytes(1)[0] < 128 ? 'heads' : 'tails';
const won = gameResult === side;
const multiplier = 2;
const payout = won ? amount * multiplier : 0;
const newBalance = currentBalance - amount + 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: amount
};
});
return json(result);
} catch (e) {
console.error('Coinflip API error:', e);
const errorMessage = e instanceof Error ? e.message : 'Internal server error';
return json({ error: errorMessage }, { status: 400 });
}
};

View file

@ -0,0 +1,105 @@
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 SlotsRequest {
amount: number;
}
function getRandomSymbol(symbols: string[]): string {
const randomValue = randomBytes(1)[0];
const index = Math.floor((randomValue / 256) * symbols.length);
return symbols[index];
}
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 { amount }: SlotsRequest = await request.json();
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 symbols = ['bliptext', 'bussin', 'griddycode', 'lyntr', 'subterfuge', 'twoblade', 'wattesigma', 'webx'];
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);
if (amount > currentBalance) {
throw new Error(`Insufficient funds. You need *${amount.toFixed(2)} but only have *${currentBalance.toFixed(2)}`);
}
// Generate random symbols
const gameResult = [
getRandomSymbol(symbols),
getRandomSymbol(symbols),
getRandomSymbol(symbols)
];
let multiplier = 0;
let winType = '';
if (gameResult[0] === gameResult[1] && gameResult[1] === gameResult[2]) {
multiplier = 5;
winType = '3 OF A KIND';
}
else if (gameResult[0] === gameResult[1] || gameResult[1] === gameResult[2] || gameResult[0] === gameResult[2]) {
multiplier = 2;
winType = '2 OF A KIND';
}
const won = multiplier > 0;
const payout = won ? amount * multiplier : 0;
const newBalance = currentBalance - amount + payout;
await tx
.update(user)
.set({
baseCurrencyBalance: newBalance.toFixed(8),
updatedAt: new Date()
})
.where(eq(user.id, userId));
return {
won,
symbols: gameResult,
newBalance,
payout,
amountWagered: amount,
winType: won ? winType : undefined
};
});
return json(result);
} catch (e) {
console.error('Slots API error:', e);
const errorMessage = e instanceof Error ? e.message : 'Internal server error';
return json({ error: errorMessage }, { status: 400 });
}
};

View file

@ -0,0 +1,85 @@
<script lang="ts">
import Coinflip from '$lib/components/self/games/Coinflip.svelte';
import Slots from '$lib/components/self/games/Slots.svelte';
import { USER_DATA } from '$lib/stores/user-data';
import { PORTFOLIO_DATA, fetchPortfolioData } from '$lib/stores/portfolio-data';
import { onMount } from 'svelte';
import { toast } from 'svelte-sonner';
import SignInConfirmDialog from '$lib/components/self/SignInConfirmDialog.svelte';
import { Button } from '$lib/components/ui/button';
let shouldSignIn = $state(false);
let balance = $state(0);
let loading = $state(true);
let activeGame = $state('coinflip');
function handleBalanceUpdate(newBalance: number) {
balance = newBalance;
if ($PORTFOLIO_DATA) {
PORTFOLIO_DATA.update((data) =>
data
? {
...data,
baseCurrencyBalance: newBalance,
totalValue: newBalance + data.totalCoinValue
}
: null
);
}
}
$effect(() => {
if ($USER_DATA && $PORTFOLIO_DATA) {
balance = $PORTFOLIO_DATA.baseCurrencyBalance;
}
});
</script>
<SignInConfirmDialog bind:open={shouldSignIn} />
<svelte:head>
<title>Gambling - Rugplay</title>
</svelte:head>
<div class="container mx-auto max-w-4xl p-6">
<h1 class="mb-6 text-center text-3xl font-bold">Gambling</h1>
{#if !$USER_DATA}
<div class="flex h-96 items-center justify-center">
<div class="text-center">
<div class="text-muted-foreground mb-4 text-xl">Sign in to start gambling</div>
<p class="text-muted-foreground mb-4 text-sm">You need an account to place bets</p>
<button
class="text-primary underline hover:cursor-pointer"
onclick={() => (shouldSignIn = true)}
>
Sign in to continue
</button>
</div>
</div>
{:else}
<!-- Game Selection -->
<div class="mb-6 flex justify-center gap-4">
<Button
variant={activeGame === 'coinflip' ? 'default' : 'outline'}
onclick={() => (activeGame = 'coinflip')}
>
Coinflip
</Button>
<Button
variant={activeGame === 'slots' ? 'default' : 'outline'}
onclick={() => (activeGame = 'slots')}
>
Slots
</Button>
</div>
<!-- Game Content -->
{#if activeGame === 'coinflip'}
<Coinflip bind:balance onBalanceUpdate={handleBalanceUpdate} />
{:else if activeGame === 'slots'}
<Slots bind:balance onBalanceUpdate={handleBalanceUpdate} />
{/if}
{/if}
</div>