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:
parent
11197d1382
commit
39991cb6c3
12 changed files with 780 additions and 67 deletions
|
|
@ -314,8 +314,20 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(async () => {
|
||||||
volumeSettings.load();
|
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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
480
website/src/lib/components/self/games/Dice.svelte
Normal file
480
website/src/lib/components/self/games/Dice.svelte
Normal file
|
|
@ -0,0 +1,480 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { Input } from '$lib/components/ui/input';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle
|
||||||
|
} from '$lib/components/ui/card';
|
||||||
|
import confetti from 'canvas-confetti';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { formatValue, playSound, showConfetti, showSchoolPrideCannons } from '$lib/utils';
|
||||||
|
import { volumeSettings } from '$lib/stores/volume-settings';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
interface DiceResult {
|
||||||
|
won: boolean;
|
||||||
|
result: number;
|
||||||
|
newBalance: number;
|
||||||
|
payout: number;
|
||||||
|
amountWagered: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_BET_AMOUNT = 1000000;
|
||||||
|
const baseRotation = "rotate3d(1, 1, 0, 340deg)";
|
||||||
|
|
||||||
|
const faceRotations = {
|
||||||
|
1: { x: 0, y: 0 },
|
||||||
|
2: { x: 0, y: 90 },
|
||||||
|
3: { x: 90, y: 0 },
|
||||||
|
4: { x: -90, y: 0 },
|
||||||
|
5: { x: 0, y: -90 },
|
||||||
|
6: { x: 0, y: 180 }
|
||||||
|
};
|
||||||
|
|
||||||
|
const diceRotations = {
|
||||||
|
1: { x: 0, y: 0, z: 0 },
|
||||||
|
2: { x: 0, y: -90, z: 0 },
|
||||||
|
3: { x: -90, y: 0, z: 0 },
|
||||||
|
4: { x: 90, y: 0, z: 0 },
|
||||||
|
5: { x: 0, y: 90, z: 0 },
|
||||||
|
6: { x: 0, y: 180, z: 0 }
|
||||||
|
};
|
||||||
|
|
||||||
|
function getRandInt(from: number, to: number): number {
|
||||||
|
return Math.round(Math.random() * (to - from)) + from;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExtraSpin(spinFactor = 4) {
|
||||||
|
const extraSpinsX = spinFactor * 360;
|
||||||
|
const extraSpinsY = spinFactor * 360;
|
||||||
|
const extraSpinsZ = spinFactor * 360;
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: extraSpinsX,
|
||||||
|
y: extraSpinsY,
|
||||||
|
z: extraSpinsZ
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFaceRotation(face: number) {
|
||||||
|
return faceRotations[face as keyof typeof faceRotations];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFaceTransform(face: number): string {
|
||||||
|
const rotation = getFaceRotation(face);
|
||||||
|
return `${getRotate(rotation.x, rotation.y)} translateZ(50px)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDiceRotation(face: number, addExtraSpin = false, spinFactor = 4) {
|
||||||
|
let extraSpin = { x: 0, y: 0, z: 0 };
|
||||||
|
|
||||||
|
if (addExtraSpin) {
|
||||||
|
extraSpin = getExtraSpin(spinFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rotation = diceRotations[face as keyof typeof diceRotations];
|
||||||
|
return {
|
||||||
|
x: rotation.x + extraSpin.x,
|
||||||
|
y: rotation.y + extraSpin.y,
|
||||||
|
z: rotation.z + extraSpin.z
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDiceTransform(face: number, addExtraSpin = false, spinFactor = 4): string {
|
||||||
|
const rotation = getDiceRotation(face, addExtraSpin, spinFactor);
|
||||||
|
return `${baseRotation} ${getRotate(rotation.x, rotation.y, rotation.z)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRotate(x?: number, y?: number, z?: number) {
|
||||||
|
const rotateX = x !== undefined ? `rotateX(${x}deg)` : "";
|
||||||
|
const rotateY = y !== undefined ? `rotateY(${y}deg)` : "";
|
||||||
|
const rotateZ = z !== undefined ? `rotateZ(${z}deg)` : "";
|
||||||
|
return `${rotateX} ${rotateY} ${rotateZ}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
balance = $bindable(),
|
||||||
|
onBalanceUpdate
|
||||||
|
}: {
|
||||||
|
balance: number;
|
||||||
|
onBalanceUpdate?: (newBalance: number) => void;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let betAmount = $state(10);
|
||||||
|
let betAmountDisplay = $state('10');
|
||||||
|
let selectedNumber = $state(1);
|
||||||
|
let isRolling = $state(false);
|
||||||
|
let diceRotation = $state({ x: 0, y: 0 });
|
||||||
|
let lastResult = $state<DiceResult | null>(null);
|
||||||
|
let activeSoundTimeouts = $state<NodeJS.Timeout[]>([]);
|
||||||
|
|
||||||
|
let canBet = $derived(
|
||||||
|
betAmount > 0 && betAmount <= balance && betAmount <= MAX_BET_AMOUNT && !isRolling
|
||||||
|
);
|
||||||
|
|
||||||
|
function selectNumber(num: number) {
|
||||||
|
if (!isRolling) {
|
||||||
|
selectedNumber = num;
|
||||||
|
playSound('click');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBetAmount(amount: number) {
|
||||||
|
const clampedAmount = Math.min(amount, Math.min(balance, MAX_BET_AMOUNT));
|
||||||
|
if (clampedAmount >= 0) {
|
||||||
|
betAmount = clampedAmount;
|
||||||
|
betAmountDisplay = clampedAmount.toLocaleString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBetAmountInput(event: Event) {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
const value = target.value.replace(/,/g, '');
|
||||||
|
const numValue = parseFloat(value) || 0;
|
||||||
|
const clampedValue = Math.min(numValue, Math.min(balance, MAX_BET_AMOUNT));
|
||||||
|
|
||||||
|
betAmount = clampedValue;
|
||||||
|
betAmountDisplay = target.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBetAmountBlur() {
|
||||||
|
betAmountDisplay = betAmount.toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rollDice() {
|
||||||
|
if (!canBet) return;
|
||||||
|
|
||||||
|
isRolling = true;
|
||||||
|
lastResult = null;
|
||||||
|
|
||||||
|
activeSoundTimeouts.forEach(clearTimeout);
|
||||||
|
activeSoundTimeouts = [];
|
||||||
|
|
||||||
|
const spinFactor = 20; // Increase / Decrease to make the Spin faster or slower
|
||||||
|
const animationDuration = 1500; // Duration of the Animation, keep it like thatif you haven't added your own sound in website\static\sound\dice.mp3
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/gambling/dice', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
selectedNumber,
|
||||||
|
amount: betAmount
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error || 'Failed to place bet');
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultData: DiceResult = await response.json();
|
||||||
|
|
||||||
|
playSound('dice');
|
||||||
|
const diceElement = document.querySelector('.dice') as HTMLElement;
|
||||||
|
if (diceElement) {
|
||||||
|
diceElement.style.transition = 'none';
|
||||||
|
diceElement.style.transform = getDiceTransform(selectedNumber, false);
|
||||||
|
void diceElement.offsetHeight;
|
||||||
|
diceElement.style.transition = 'transform 1.5s cubic-bezier(0.1, 0.9, 0.1, 1)';
|
||||||
|
diceElement.style.transform = getDiceTransform(resultData.result, true, spinFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, animationDuration));
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 200)); // Small delay to show the Result
|
||||||
|
|
||||||
|
balance = resultData.newBalance;
|
||||||
|
lastResult = resultData;
|
||||||
|
onBalanceUpdate?.(resultData.newBalance);
|
||||||
|
|
||||||
|
if (resultData.won) {
|
||||||
|
showConfetti(confetti);
|
||||||
|
showSchoolPrideCannons(confetti);
|
||||||
|
} else {
|
||||||
|
playSound('lose');
|
||||||
|
}
|
||||||
|
|
||||||
|
isRolling = false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Dice roll error:', error);
|
||||||
|
toast.error('Roll failed', {
|
||||||
|
description: error instanceof Error ? error.message : 'Unknown error occurred'
|
||||||
|
});
|
||||||
|
isRolling = false;
|
||||||
|
activeSoundTimeouts.forEach(clearTimeout);
|
||||||
|
activeSoundTimeouts = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDotsForFace(face: number): number { // Could be made bigger, has no Point though Ig.
|
||||||
|
switch (face) {
|
||||||
|
case 1: return 1;
|
||||||
|
case 2: return 2;
|
||||||
|
case 3: return 3;
|
||||||
|
case 4: return 4;
|
||||||
|
case 5: return 5;
|
||||||
|
case 6: return 6;
|
||||||
|
default: return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Dice</CardTitle>
|
||||||
|
<CardDescription>Choose a number and roll the dice to win 3x your bet!</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div class="grid grid-cols-1 gap-8 md:grid-cols-2">
|
||||||
|
<div class="flex flex-col space-y-4">
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="text-muted-foreground text-sm">Balance</p>
|
||||||
|
<p class="text-2xl font-bold">{formatValue(balance)}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 flex items-center justify-center">
|
||||||
|
<div class="dice-container">
|
||||||
|
<div class="dice">
|
||||||
|
{#each Array(6) as _, i}
|
||||||
|
<div class="face" style="transform: {getFaceTransform(i + 1)}">
|
||||||
|
<div class="dot-container">
|
||||||
|
{#each Array(getDotsForFace(i + 1)) as _}
|
||||||
|
<div class="dot"></div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-center text-center">
|
||||||
|
{#if lastResult && !isRolling}
|
||||||
|
<div class="bg-muted/50 w-full rounded-lg p-3">
|
||||||
|
{#if lastResult.won}
|
||||||
|
<p class="text-success font-semibold">WIN</p>
|
||||||
|
<p class="text-sm">
|
||||||
|
Won {formatValue(lastResult.payout)} on {lastResult.result}
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<p class="text-destructive font-semibold">LOSS</p>
|
||||||
|
<p class="text-sm">
|
||||||
|
Lost {formatValue(lastResult.amountWagered)} on {lastResult.result}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<div class="mb-2 block text-sm font-medium">Choose Number</div>
|
||||||
|
<div class="grid grid-cols-3 gap-2">
|
||||||
|
{#each Array(6) as _, i}
|
||||||
|
<Button
|
||||||
|
variant={selectedNumber === i + 1 ? 'default' : 'outline'}
|
||||||
|
onclick={() => selectNumber(i + 1)}
|
||||||
|
disabled={isRolling}
|
||||||
|
class="h-16"
|
||||||
|
>
|
||||||
|
{i + 1}
|
||||||
|
</Button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="bet-amount" class="mb-2 block text-sm font-medium">Bet Amount</label>
|
||||||
|
<Input
|
||||||
|
id="bet-amount"
|
||||||
|
type="text"
|
||||||
|
value={betAmountDisplay}
|
||||||
|
oninput={handleBetAmountInput}
|
||||||
|
onblur={handleBetAmountBlur}
|
||||||
|
disabled={isRolling}
|
||||||
|
placeholder="Enter bet amount"
|
||||||
|
/>
|
||||||
|
<p class="text-muted-foreground mt-1 text-xs">
|
||||||
|
Max bet: {MAX_BET_AMOUNT.toLocaleString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="grid grid-cols-4 gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onclick={() =>
|
||||||
|
setBetAmount(Math.floor(Math.min(balance || 0, MAX_BET_AMOUNT) * 0.25))}
|
||||||
|
disabled={isRolling}>25%</Button
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onclick={() => setBetAmount(Math.floor(Math.min(balance || 0, MAX_BET_AMOUNT) * 0.5))}
|
||||||
|
disabled={isRolling}>50%</Button
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onclick={() =>
|
||||||
|
setBetAmount(Math.floor(Math.min(balance || 0, MAX_BET_AMOUNT) * 0.75))}
|
||||||
|
disabled={isRolling}>75%</Button
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onclick={() => setBetAmount(Math.floor(Math.min(balance || 0, MAX_BET_AMOUNT)))}
|
||||||
|
disabled={isRolling}>Max</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button class="h-12 w-full text-lg" onclick={rollDice} disabled={!canBet}>
|
||||||
|
{isRolling ? 'Rolling...' : 'Roll'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.dice-container {
|
||||||
|
perspective: 1000px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dice {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
transform: rotate3d(0.9, 1, 0, 340deg);
|
||||||
|
transition: transform 4s cubic-bezier(0.1, 0.9, 0.1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.face {
|
||||||
|
position: absolute;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
background: #fff;
|
||||||
|
border: 2px solid #363131;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.face:nth-child(1) { transform: translateZ(50px); }
|
||||||
|
.face:nth-child(2) { transform: rotateY(90deg) translateZ(50px); }
|
||||||
|
.face:nth-child(3) { transform: rotateX(90deg) translateZ(50px); }
|
||||||
|
.face:nth-child(4) { transform: rotateX(-90deg) translateZ(50px); }
|
||||||
|
.face:nth-child(5) { transform: rotateY(-90deg) translateZ(50px); }
|
||||||
|
.face:nth-child(6) { transform: rotateY(180deg) translateZ(50px); }
|
||||||
|
|
||||||
|
.dot-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
grid-template-rows: repeat(3, 1fr);
|
||||||
|
padding: 15%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
gap: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
background: #363131;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.face:nth-child(1) .dot-container {
|
||||||
|
grid-template-areas:
|
||||||
|
". . ."
|
||||||
|
". dot ."
|
||||||
|
". . .";
|
||||||
|
}
|
||||||
|
.face:nth-child(1) .dot {
|
||||||
|
grid-area: dot;
|
||||||
|
}
|
||||||
|
|
||||||
|
.face:nth-child(2) .dot-container {
|
||||||
|
grid-template-areas:
|
||||||
|
"dot1 . ."
|
||||||
|
". . ."
|
||||||
|
". . dot2";
|
||||||
|
}
|
||||||
|
.face:nth-child(2) .dot:nth-child(1) { grid-area: dot1; }
|
||||||
|
.face:nth-child(2) .dot:nth-child(2) { grid-area: dot2; }
|
||||||
|
|
||||||
|
.face:nth-child(3) .dot-container {
|
||||||
|
grid-template-areas:
|
||||||
|
"dot1 . ."
|
||||||
|
". dot2 ."
|
||||||
|
". . dot3";
|
||||||
|
}
|
||||||
|
.face:nth-child(3) .dot:nth-child(1) { grid-area: dot1; }
|
||||||
|
.face:nth-child(3) .dot:nth-child(2) { grid-area: dot2; }
|
||||||
|
.face:nth-child(3) .dot:nth-child(3) { grid-area: dot3; }
|
||||||
|
|
||||||
|
.face:nth-child(4) .dot-container {
|
||||||
|
grid-template-areas:
|
||||||
|
"dot1 . dot2"
|
||||||
|
". . ."
|
||||||
|
"dot3 . dot4";
|
||||||
|
}
|
||||||
|
.face:nth-child(4) .dot:nth-child(1) { grid-area: dot1; }
|
||||||
|
.face:nth-child(4) .dot:nth-child(2) { grid-area: dot2; }
|
||||||
|
.face:nth-child(4) .dot:nth-child(3) { grid-area: dot3; }
|
||||||
|
.face:nth-child(4) .dot:nth-child(4) { grid-area: dot4; }
|
||||||
|
|
||||||
|
.face:nth-child(5) .dot-container {
|
||||||
|
grid-template-areas:
|
||||||
|
"dot1 . dot2"
|
||||||
|
". dot3 ."
|
||||||
|
"dot4 . dot5";
|
||||||
|
}
|
||||||
|
.face:nth-child(5) .dot:nth-child(1) { grid-area: dot1; }
|
||||||
|
.face:nth-child(5) .dot:nth-child(2) { grid-area: dot2; }
|
||||||
|
.face:nth-child(5) .dot:nth-child(3) { grid-area: dot3; }
|
||||||
|
.face:nth-child(5) .dot:nth-child(4) { grid-area: dot4; }
|
||||||
|
.face:nth-child(5) .dot:nth-child(5) { grid-area: dot5; }
|
||||||
|
|
||||||
|
.face:nth-child(6) .dot-container {
|
||||||
|
grid-template-areas:
|
||||||
|
"dot1 . dot2"
|
||||||
|
"dot3 . dot4"
|
||||||
|
"dot5 . dot6";
|
||||||
|
}
|
||||||
|
.face:nth-child(6) .dot:nth-child(1) { grid-area: dot1; }
|
||||||
|
.face:nth-child(6) .dot:nth-child(2) { grid-area: dot2; }
|
||||||
|
.face:nth-child(6) .dot:nth-child(3) { grid-area: dot3; }
|
||||||
|
.face:nth-child(6) .dot:nth-child(4) { grid-area: dot4; }
|
||||||
|
.face:nth-child(6) .dot:nth-child(5) { grid-area: dot5; }
|
||||||
|
.face:nth-child(6) .dot:nth-child(6) { grid-area: dot6; }
|
||||||
|
</style>
|
||||||
|
|
@ -131,6 +131,7 @@
|
||||||
}
|
}
|
||||||
if (autoCashoutTimer >= AUTO_CASHOUT_TIME) {
|
if (autoCashoutTimer >= AUTO_CASHOUT_TIME) {
|
||||||
isAutoCashout = true;
|
isAutoCashout = true;
|
||||||
|
clearInterval(autoCashoutInterval);
|
||||||
cashOut();
|
cashOut();
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
@ -227,6 +228,12 @@
|
||||||
hasRevealedTile = false;
|
hasRevealedTile = false;
|
||||||
isAutoCashout = false;
|
isAutoCashout = false;
|
||||||
resetAutoCashoutTimer();
|
resetAutoCashoutTimer();
|
||||||
|
|
||||||
|
// Prevents the Tiles getting revealed when you Abort your bet.
|
||||||
|
if (!result.isAbort) {
|
||||||
|
revealedTiles = [...Array(TOTAL_TILES).keys()];
|
||||||
|
minePositions = result.minePositions;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Cashout error:', error);
|
console.error('Cashout error:', error);
|
||||||
toast.error('Failed to cash out', {
|
toast.error('Failed to cash out', {
|
||||||
|
|
@ -276,8 +283,21 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
// Dynmaically fetch the correct balance.
|
||||||
|
onMount(async () => {
|
||||||
volumeSettings.load();
|
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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
|
|
@ -359,7 +379,7 @@
|
||||||
<button
|
<button
|
||||||
class="mine-tile"
|
class="mine-tile"
|
||||||
class:revealed={revealedTiles.includes(index)}
|
class:revealed={revealedTiles.includes(index)}
|
||||||
class:mine={revealedTiles.includes(index) && minePositions.includes(index) && index === lastClickedTile}
|
class:mine={revealedTiles.includes(index) && minePositions.includes(index) && !clickedSafeTiles.includes(index)}
|
||||||
class:safe={revealedTiles.includes(index) && !minePositions.includes(index) && clickedSafeTiles.includes(index)}
|
class:safe={revealedTiles.includes(index) && !minePositions.includes(index) && clickedSafeTiles.includes(index)}
|
||||||
class:light={document.documentElement.classList.contains('light')}
|
class:light={document.documentElement.classList.contains('light')}
|
||||||
onclick={() => handleTileClick(index)}
|
onclick={() => handleTileClick(index)}
|
||||||
|
|
@ -390,22 +410,29 @@
|
||||||
<!-- Mine Count Selector -->
|
<!-- Mine Count Selector -->
|
||||||
<div>
|
<div>
|
||||||
<label for="mine-count" class="block text-sm font-medium mb-2">Number of Mines</label>
|
<label for="mine-count" class="block text-sm font-medium mb-2">Number of Mines</label>
|
||||||
<div id="mine-count">
|
<div id="mine-count" class="flex items-center gap-2">
|
||||||
<Select
|
<div class="flex gap-1 items-center">
|
||||||
type="single"
|
<Button
|
||||||
value={mineCount.toString()}
|
size="sm"
|
||||||
onValueChange={(value: string) => (mineCount = parseInt(value))}
|
variant="outline"
|
||||||
disabled={isPlaying}
|
class="w-8 h-8 px-2 flex items-center justify-center"
|
||||||
|
onclick={() => mineCount = Math.max(MIN_MINES, mineCount - 1)}
|
||||||
|
disabled={isPlaying || mineCount <= MIN_MINES}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
-
|
||||||
<span>{mineCount} Mines</span>
|
</Button>
|
||||||
</SelectTrigger>
|
<div class="text-center font-medium">{mineCount}</div>
|
||||||
<SelectContent>
|
<Button
|
||||||
{#each Array(22) as _, i}
|
size="sm"
|
||||||
<SelectItem value={(i + MIN_MINES).toString()}>{i + MIN_MINES} Mines</SelectItem>
|
variant="outline"
|
||||||
{/each}
|
class="w-8 h-8 px-2 flex items-center justify-center"
|
||||||
</SelectContent>
|
onclick={() => mineCount = Math.min(24, mineCount + 1)}
|
||||||
</Select>
|
disabled={isPlaying || mineCount >= 24}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</Button>
|
||||||
|
<p>Mines</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -211,8 +211,21 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onMount(() => {
|
// Dynmaically fetch the correct balance.
|
||||||
|
onMount(async () => {
|
||||||
volumeSettings.load();
|
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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,17 +19,13 @@ interface MinesSession {
|
||||||
|
|
||||||
export const activeGames = new Map<string, MinesSession>();
|
export const activeGames = new Map<string, MinesSession>();
|
||||||
|
|
||||||
// Clean up old games every minute.
|
// Clean up old games every minute. (5 Minute system)
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
for (const [token, game] of activeGames.entries()) {
|
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 (now - game.lastActivity > 5 * 60 * 1000) {
|
||||||
// If no tiles were revealed, refund the bet
|
|
||||||
if (game.revealedTiles.length === 0) {
|
if (game.revealedTiles.length === 0) {
|
||||||
try {
|
try {
|
||||||
console.log(`Processing refund for inactive Mines game ${token} (User: ${game.userId}, Bet: ${game.betAmount})`);
|
|
||||||
|
|
||||||
const [userData] = await db
|
const [userData] = await db
|
||||||
.select({ baseCurrencyBalance: user.baseCurrencyBalance })
|
.select({ baseCurrencyBalance: user.baseCurrencyBalance })
|
||||||
.from(user)
|
.from(user)
|
||||||
|
|
@ -48,29 +44,74 @@ setInterval(async () => {
|
||||||
})
|
})
|
||||||
.where(eq(user.id, game.userId));
|
.where(eq(user.id, game.userId));
|
||||||
|
|
||||||
console.log(`Successfully refunded ${game.betAmount} to user ${game.userId}. New balance: ${newBalance}`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to refund inactive game ${token}:`, 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);
|
activeGames.delete(token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 60000);
|
}, 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...
|
// Rig the game...
|
||||||
const getMaxPayout = (bet: number, picks: number): number => {
|
const getMaxPayout = (bet: number, picks: number, mines: number): number => {
|
||||||
const absoluteCap = 5_000_000; // never pay above this. Yeah, its rigged. Live with that :)
|
const MAX_PAYOUT = 2_000_000; // Maximum payout cap of 2 million to not make linker too rich
|
||||||
const baseCap = 1.4; // 1.4x min multiplier, increase to, well, increase payouts
|
const HIGH_BET_THRESHOLD = 50_000;
|
||||||
const growthRate = 0.45; // cap curve sensitivity
|
|
||||||
|
|
||||||
// Cap increases with number of successful reveals
|
const mineFactor = 1 + (mines / 25);
|
||||||
const effectiveMultiplierCap = baseCap + Math.pow(picks, growthRate);
|
const baseMultiplier = (1.4 + Math.pow(picks, 0.45)) * mineFactor;
|
||||||
const payoutCap = bet * effectiveMultiplierCap;
|
|
||||||
|
|
||||||
return Math.min(payoutCap, absoluteCap);
|
// 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 TOTAL_TILES = 25;
|
||||||
const HOUSE_EDGE = 0.05;
|
const HOUSE_EDGE = 0.05;
|
||||||
|
|
||||||
|
// Calculate probability of winning based on picks and mines
|
||||||
let probability = 1;
|
let probability = 1;
|
||||||
for (let i = 0; i < picks; i++) {
|
for (let i = 0; i < picks; i++) {
|
||||||
probability *= (TOTAL_TILES - mines - i) / (TOTAL_TILES - 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;
|
if (probability <= 0) return 1.0;
|
||||||
|
|
||||||
|
// Calculate fair multiplier based on probability and house edge
|
||||||
const fairMultiplier = (1 / probability) * (1 - HOUSE_EDGE);
|
const fairMultiplier = (1 / probability) * (1 - HOUSE_EDGE);
|
||||||
const rawPayout = fairMultiplier * betAmount;
|
|
||||||
|
|
||||||
const maxPayout = getMaxPayout(betAmount, picks);
|
const rawPayout = fairMultiplier * betAmount;
|
||||||
|
const maxPayout = getMaxPayout(betAmount, picks, mines);
|
||||||
const cappedPayout = Math.min(rawPayout, maxPayout);
|
const cappedPayout = Math.min(rawPayout, maxPayout);
|
||||||
const effectiveMultiplier = cappedPayout / betAmount;
|
const effectiveMultiplier = cappedPayout / betAmount;
|
||||||
|
|
||||||
return Math.max(1.0, effectiveMultiplier);
|
return Math.max(1.0, Number(effectiveMultiplier.toFixed(2)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,11 +95,7 @@
|
||||||
class="text-primary underline hover:cursor-pointer"
|
class="text-primary underline hover:cursor-pointer"
|
||||||
onclick={() => (shouldSignIn = !shouldSignIn)}>sign in</button
|
onclick={() => (shouldSignIn = !shouldSignIn)}>sign in</button
|
||||||
>
|
>
|
||||||
or{' '}
|
to play.
|
||||||
<button
|
|
||||||
class="text-primary underline hover:cursor-pointer"
|
|
||||||
onclick={() => (shouldSignIn = !shouldSignIn)}>create an account</button
|
|
||||||
> to play.
|
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
87
website/src/routes/api/gambling/dice/+server.ts
Normal file
87
website/src/routes/api/gambling/dice/+server.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -47,6 +47,7 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
newBalance = Math.round((currentBalance + roundedPayout) * 100000000) / 100000000;
|
newBalance = Math.round((currentBalance + roundedPayout) * 100000000) / 100000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
await tx
|
await tx
|
||||||
.update(user)
|
.update(user)
|
||||||
.set({
|
.set({
|
||||||
|
|
@ -55,13 +56,15 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
})
|
})
|
||||||
.where(eq(user.id, userId));
|
.where(eq(user.id, userId));
|
||||||
|
|
||||||
|
|
||||||
activeGames.delete(sessionToken);
|
activeGames.delete(sessionToken);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
newBalance,
|
newBalance,
|
||||||
payout,
|
payout,
|
||||||
amountWagered: game.betAmount,
|
amountWagered: game.betAmount,
|
||||||
isAbort: game.revealedTiles.length === 0
|
isAbort: game.revealedTiles.length === 0,
|
||||||
|
minePositions: game.minePositions
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,35 +27,46 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
return json({ error: 'Tile already revealed' }, { status: 400 });
|
return json({ error: 'Tile already revealed' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update last activity time
|
|
||||||
game.lastActivity = Date.now();
|
game.lastActivity = Date.now();
|
||||||
|
|
||||||
// Check if hit mine
|
|
||||||
|
|
||||||
if (game.minePositions.includes(tileIndex)) {
|
if (game.minePositions.includes(tileIndex)) {
|
||||||
game.status = 'lost';
|
game.status = 'lost';
|
||||||
const minePositions = game.minePositions;
|
const minePositions = game.minePositions;
|
||||||
|
|
||||||
// Fetch user balance to return after loss
|
|
||||||
const userId = Number(session.user.id);
|
const userId = Number(session.user.id);
|
||||||
const [userData] = await db
|
const [userData] = await db
|
||||||
.select({ baseCurrencyBalance: user.baseCurrencyBalance })
|
.select({ baseCurrencyBalance: user.baseCurrencyBalance })
|
||||||
.from(user)
|
.from(user)
|
||||||
.where(eq(user.id, userId))
|
.where(eq(user.id, userId))
|
||||||
|
.for('update')
|
||||||
.limit(1);
|
.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);
|
activeGames.delete(sessionToken);
|
||||||
|
|
||||||
|
|
||||||
return json({
|
return json({
|
||||||
hitMine: true,
|
hitMine: true,
|
||||||
minePositions,
|
minePositions,
|
||||||
newBalance: Number(userData.baseCurrencyBalance),
|
newBalance: currentBalance,
|
||||||
status: 'lost'
|
status: 'lost',
|
||||||
|
amountWagered: game.betAmount
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Safe tile
|
// Safe tile (Yipeee)
|
||||||
game.revealedTiles.push(tileIndex);
|
game.revealedTiles.push(tileIndex);
|
||||||
game.currentMultiplier = calculateMultiplier(
|
game.currentMultiplier = calculateMultiplier(
|
||||||
game.revealedTiles.length,
|
game.revealedTiles.length,
|
||||||
|
|
@ -63,9 +74,38 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
game.betAmount
|
game.betAmount
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if all safe tiles are revealed. Crazy when you get this :)
|
|
||||||
if (game.revealedTiles.length === 25 - game.mineCount) {
|
if (game.revealedTiles.length === 25 - game.mineCount) {
|
||||||
game.status = 'won';
|
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({
|
return json({
|
||||||
|
|
|
||||||
|
|
@ -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)}`);
|
throw new Error(`Insufficient funds. You need *${roundedAmount.toFixed(2)} but only have *${roundedBalance.toFixed(2)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Generate mine positions
|
// Generate mine positions
|
||||||
const positions = new Set<number>();
|
const positions = new Set<number>();
|
||||||
while (positions.size < mineCount) {
|
while (positions.size < mineCount) {
|
||||||
|
|
@ -52,11 +53,8 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
for (let i = 0; i < 25; i++) {
|
for (let i = 0; i < 25; i++) {
|
||||||
if (!positions.has(i)) safePositions.push(i);
|
if (!positions.has(i)) safePositions.push(i);
|
||||||
}
|
}
|
||||||
console.log(positions)
|
|
||||||
console.log('Safe positions:', safePositions);
|
|
||||||
|
|
||||||
|
// transaction token for authentication stuff
|
||||||
// transaction token for authentication
|
|
||||||
const randomBytes = new Uint8Array(8);
|
const randomBytes = new Uint8Array(8);
|
||||||
crypto.getRandomValues(randomBytes);
|
crypto.getRandomValues(randomBytes);
|
||||||
const sessionToken = Array.from(randomBytes)
|
const sessionToken = Array.from(randomBytes)
|
||||||
|
|
@ -64,6 +62,7 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
const newBalance = roundedBalance - roundedAmount;
|
||||||
|
|
||||||
// Create session
|
// Create session
|
||||||
activeGames.set(sessionToken, {
|
activeGames.set(sessionToken, {
|
||||||
|
|
@ -79,16 +78,20 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
userId
|
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
|
await tx
|
||||||
.update(user)
|
.update(user)
|
||||||
.set({
|
.set({
|
||||||
baseCurrencyBalance: (roundedBalance - roundedAmount).toFixed(8),
|
baseCurrencyBalance: newBalance.toFixed(8),
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
})
|
})
|
||||||
.where(eq(user.id, userId));
|
.where(eq(user.id, userId));
|
||||||
|
|
||||||
return { sessionToken };
|
|
||||||
|
return {
|
||||||
|
sessionToken,
|
||||||
|
newBalance
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return json(result);
|
return json(result);
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
import SignInConfirmDialog from '$lib/components/self/SignInConfirmDialog.svelte';
|
import SignInConfirmDialog from '$lib/components/self/SignInConfirmDialog.svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import SEO from '$lib/components/self/SEO.svelte';
|
import SEO from '$lib/components/self/SEO.svelte';
|
||||||
|
import Dice from '$lib/components/self/games/Dice.svelte'
|
||||||
|
|
||||||
let shouldSignIn = $state(false);
|
let shouldSignIn = $state(false);
|
||||||
let balance = $state(0);
|
let balance = $state(0);
|
||||||
|
|
@ -78,6 +79,12 @@
|
||||||
>
|
>
|
||||||
Mines
|
Mines
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={activeGame === 'dice' ? 'default' : 'outline'}
|
||||||
|
onclick={() => (activeGame = 'dice')}
|
||||||
|
>
|
||||||
|
Dice
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Game Content -->
|
<!-- Game Content -->
|
||||||
|
|
@ -87,6 +94,8 @@
|
||||||
<Slots bind:balance onBalanceUpdate={handleBalanceUpdate} />
|
<Slots bind:balance onBalanceUpdate={handleBalanceUpdate} />
|
||||||
{:else if activeGame === 'mines'}
|
{:else if activeGame === 'mines'}
|
||||||
<Mines bind:balance onBalanceUpdate={handleBalanceUpdate} />
|
<Mines bind:balance onBalanceUpdate={handleBalanceUpdate} />
|
||||||
|
{:else if activeGame === 'dice'}
|
||||||
|
<Dice bind:balance onBalanceUpdate={handleBalanceUpdate} />
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
BIN
website/static/sound/dice.mp3
Normal file
BIN
website/static/sound/dice.mp3
Normal file
Binary file not shown.
Reference in a new issue