Minegame.exe
This commit is contained in:
parent
7b11266f72
commit
11197d1382
6 changed files with 979 additions and 5 deletions
614
website/src/lib/components/self/games/Mines.svelte
Normal file
614
website/src/lib/components/self/games/Mines.svelte
Normal file
|
|
@ -0,0 +1,614 @@
|
||||||
|
<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 {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger
|
||||||
|
} from '$lib/components/ui/select';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger
|
||||||
|
} from '$lib/components/ui/dialog';
|
||||||
|
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, onDestroy } from 'svelte';
|
||||||
|
import { ModeWatcher } from 'mode-watcher';
|
||||||
|
|
||||||
|
interface MinesResult {
|
||||||
|
won: boolean;
|
||||||
|
newBalance: number;
|
||||||
|
payout: number;
|
||||||
|
amountWagered: number;
|
||||||
|
sessionToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GRID_SIZE = 5;
|
||||||
|
const TOTAL_TILES = GRID_SIZE * GRID_SIZE;
|
||||||
|
const MAX_BET_AMOUNT = 1000000;
|
||||||
|
const MIN_MINES = 3;
|
||||||
|
const AUTO_CASHOUT_TIME = 15; // 15 seconds total (10s game + 5s buffer)
|
||||||
|
|
||||||
|
let {
|
||||||
|
balance = $bindable(),
|
||||||
|
onBalanceUpdate
|
||||||
|
}: {
|
||||||
|
balance: number;
|
||||||
|
onBalanceUpdate?: (newBalance: number) => void;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let betAmount = $state(10);
|
||||||
|
let betAmountDisplay = $state('10');
|
||||||
|
let mineCount = $state(3);
|
||||||
|
let isPlaying = $state(false);
|
||||||
|
let revealedTiles = $state<number[]>([]);
|
||||||
|
let minePositions = $state<number[]>([]);
|
||||||
|
let currentMultiplier = $state(1);
|
||||||
|
let lastResult = $state<MinesResult | null>(null);
|
||||||
|
let autoCashoutTimer = $state(0);
|
||||||
|
let autoCashoutProgress = $state(0);
|
||||||
|
let sessionToken = $state<string | null>(null);
|
||||||
|
let autoCashoutInterval: ReturnType<typeof setInterval>;
|
||||||
|
let hasRevealedTile = $state(false);
|
||||||
|
let isAutoCashout = $state(false);
|
||||||
|
let lastClickedTile = $state<number | null>(null);
|
||||||
|
let clickedSafeTiles = $state<number[]>([]);
|
||||||
|
|
||||||
|
let canBet = $derived(
|
||||||
|
betAmount > 0 && betAmount <= balance && betAmount <= MAX_BET_AMOUNT && !isPlaying
|
||||||
|
);
|
||||||
|
|
||||||
|
function calculateProbability(picks: number, mines: number): string {
|
||||||
|
let probability = 1;
|
||||||
|
for (let i = 0; i < picks; i++) {
|
||||||
|
probability *= (TOTAL_TILES - mines - i) / (TOTAL_TILES - i);
|
||||||
|
}
|
||||||
|
return (probability * 100).toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateRawMultiplier(picks: number, mines: number): number {
|
||||||
|
let probability = 1;
|
||||||
|
for (let i = 0; i < picks; i++) {
|
||||||
|
probability *= (TOTAL_TILES - mines - i) / (TOTAL_TILES - i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (probability === 0) return 1.0;
|
||||||
|
return Math.max(1.0, 1 / probability);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetAutoCashoutTimer() {
|
||||||
|
if (autoCashoutInterval) {
|
||||||
|
clearInterval(autoCashoutInterval);
|
||||||
|
}
|
||||||
|
autoCashoutTimer = 0;
|
||||||
|
autoCashoutProgress = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startAutoCashoutTimer() {
|
||||||
|
if (!hasRevealedTile) return;
|
||||||
|
|
||||||
|
resetAutoCashoutTimer();
|
||||||
|
autoCashoutInterval = setInterval(() => {
|
||||||
|
if (autoCashoutTimer < AUTO_CASHOUT_TIME) {
|
||||||
|
autoCashoutTimer += 0.1;
|
||||||
|
autoCashoutProgress = (autoCashoutTimer / AUTO_CASHOUT_TIME) * 100;
|
||||||
|
}
|
||||||
|
if (autoCashoutTimer >= AUTO_CASHOUT_TIME) {
|
||||||
|
isAutoCashout = true;
|
||||||
|
cashOut();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleTileClick(index: number) {
|
||||||
|
if (!isPlaying || revealedTiles.includes(index) || !sessionToken) return;
|
||||||
|
|
||||||
|
lastClickedTile = index;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/gambling/mines/reveal', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
sessionToken,
|
||||||
|
tileIndex: index
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error || 'Failed to reveal tile');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.hitMine) {
|
||||||
|
playSound('lose');
|
||||||
|
revealedTiles = [...Array(TOTAL_TILES).keys()];
|
||||||
|
minePositions = result.minePositions;
|
||||||
|
isPlaying = false;
|
||||||
|
resetAutoCashoutTimer();
|
||||||
|
|
||||||
|
balance = result.newBalance;
|
||||||
|
onBalanceUpdate?.(result.newBalance);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
playSound('flip');
|
||||||
|
revealedTiles = [...revealedTiles, index];
|
||||||
|
clickedSafeTiles = [...clickedSafeTiles, index];
|
||||||
|
currentMultiplier = result.currentMultiplier;
|
||||||
|
hasRevealedTile = true;
|
||||||
|
startAutoCashoutTimer();
|
||||||
|
|
||||||
|
if (result.status === 'won') {
|
||||||
|
showSchoolPrideCannons(confetti);
|
||||||
|
showConfetti(confetti);
|
||||||
|
await cashOut();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Mines error:', error);
|
||||||
|
toast.error('Failed to reveal tile', {
|
||||||
|
description: error instanceof Error ? error.message : 'Unknown error occurred'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cashOut() {
|
||||||
|
if (!isPlaying || !sessionToken) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/gambling/mines/cashout', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
sessionToken
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
if (!isAutoCashout || errorData.error !== 'Invalid session') {
|
||||||
|
throw new Error(errorData.error || 'Failed to cash out');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
balance = result.newBalance;
|
||||||
|
onBalanceUpdate?.(balance);
|
||||||
|
|
||||||
|
if (result.payout > betAmount) {
|
||||||
|
showConfetti(confetti);
|
||||||
|
}
|
||||||
|
|
||||||
|
playSound(result.isAbort ? 'flip' : 'win');
|
||||||
|
isPlaying = false;
|
||||||
|
hasRevealedTile = false;
|
||||||
|
isAutoCashout = false;
|
||||||
|
resetAutoCashoutTimer();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Cashout error:', error);
|
||||||
|
toast.error('Failed to cash out', {
|
||||||
|
description: error instanceof Error ? error.message : 'Unknown error occurred'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startGame() {
|
||||||
|
if (!canBet) return;
|
||||||
|
|
||||||
|
balance -= betAmount;
|
||||||
|
onBalanceUpdate?.(balance);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/gambling/mines/start', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
betAmount,
|
||||||
|
mineCount
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
balance += betAmount;
|
||||||
|
onBalanceUpdate?.(balance);
|
||||||
|
throw new Error(errorData.error || 'Failed to start game');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
isPlaying = true;
|
||||||
|
hasRevealedTile = false;
|
||||||
|
lastResult = null;
|
||||||
|
revealedTiles = [];
|
||||||
|
clickedSafeTiles = [];
|
||||||
|
currentMultiplier = 1;
|
||||||
|
sessionToken = result.sessionToken;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Start game error:', error);
|
||||||
|
toast.error('Failed to start game', {
|
||||||
|
description: error instanceof Error ? error.message : 'Unknown error occurred'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
volumeSettings.load();
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
resetAutoCashoutTimer();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Mines</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Navigate through the minefield and cash out before hitting a mine!
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger class="text-xs text-destructive hover:underline ml-1">
|
||||||
|
Info
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent class="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Mines Game Information</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h3 class="font-semibold text-lg">Winning Probabilities</h3>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
The probability of winning increases with each safe tile you reveal. Here's how it works:
|
||||||
|
</p>
|
||||||
|
<div class="bg-muted p-4 rounded-lg space-y-3">
|
||||||
|
<div class="text-sm">
|
||||||
|
<p class="font-medium mb-2">For each tile you reveal:</p>
|
||||||
|
<ul class="list-disc pl-6 space-y-1">
|
||||||
|
<li>More mines = Higher risk = Higher potential payout</li>
|
||||||
|
<li>Fewer mines = Lower risk = Lower potential payout</li>
|
||||||
|
<li>Each safe tile increases your multiplier</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm">
|
||||||
|
<p class="font-medium mb-2">Example:</p>
|
||||||
|
<p>With 3 mines on the board:</p>
|
||||||
|
<ul class="list-disc pl-6 space-y-1">
|
||||||
|
<li>First tile: 88% chance of being safe</li>
|
||||||
|
<li>Second tile: 87% chance of being safe</li>
|
||||||
|
<li>Third tile: 86% chance of being safe</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h3 class="font-semibold text-lg">Game Rules & Information</h3>
|
||||||
|
<div class="space-y-2 text-sm">
|
||||||
|
<p class="text-muted-foreground">
|
||||||
|
If you leave the page while playing:
|
||||||
|
</p>
|
||||||
|
<ul class="list-disc pl-6 space-y-1">
|
||||||
|
<li>If you haven't revealed any tiles, your game session will be ended after 5 minutes of inactivity</li>
|
||||||
|
<li>If you have revealed tiles, the auto cashout will process your gains within 15 seconds</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div class="grid grid-cols-1 gap-8 md:grid-cols-2">
|
||||||
|
<!-- Left Side: Grid and Stats -->
|
||||||
|
<div class="flex flex-col space-y-4">
|
||||||
|
<!-- Balance Display -->
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="text-muted-foreground text-sm">Balance</p>
|
||||||
|
<p class="text-2xl font-bold">{formatValue(balance)}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mines Grid -->
|
||||||
|
<div class="mines-grid" class:pulse-warning={isPlaying && autoCashoutTimer >= 7}>
|
||||||
|
{#each Array(TOTAL_TILES) as _, index}
|
||||||
|
<ModeWatcher />
|
||||||
|
<button
|
||||||
|
class="mine-tile"
|
||||||
|
class:revealed={revealedTiles.includes(index)}
|
||||||
|
class:mine={revealedTiles.includes(index) && minePositions.includes(index) && index === lastClickedTile}
|
||||||
|
class:safe={revealedTiles.includes(index) && !minePositions.includes(index) && clickedSafeTiles.includes(index)}
|
||||||
|
class:light={document.documentElement.classList.contains('light')}
|
||||||
|
onclick={() => handleTileClick(index)}
|
||||||
|
disabled={!isPlaying}
|
||||||
|
>
|
||||||
|
{#if revealedTiles.includes(index)}
|
||||||
|
{#if minePositions.includes(index)}
|
||||||
|
<img
|
||||||
|
src="/facedev/avif/bussin.avif"
|
||||||
|
alt="Mine"
|
||||||
|
class="h-8 w-8 object-contain"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<img
|
||||||
|
src="/facedev/avif/twoblade.avif"
|
||||||
|
alt="Safe"
|
||||||
|
class="h-8 w-8 object-contain"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Side: Controls Things -->
|
||||||
|
<div class="space-y-4">
|
||||||
|
<!-- Mine Count Selector -->
|
||||||
|
<div>
|
||||||
|
<label for="mine-count" class="block text-sm font-medium mb-2">Number of Mines</label>
|
||||||
|
<div id="mine-count">
|
||||||
|
<Select
|
||||||
|
type="single"
|
||||||
|
value={mineCount.toString()}
|
||||||
|
onValueChange={(value: string) => (mineCount = parseInt(value))}
|
||||||
|
disabled={isPlaying}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<span>{mineCount} Mines</span>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{#each Array(22) as _, i}
|
||||||
|
<SelectItem value={(i + MIN_MINES).toString()}>{i + MIN_MINES} Mines</SelectItem>
|
||||||
|
{/each}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bet Amount -->
|
||||||
|
<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={isPlaying}
|
||||||
|
placeholder="Enter bet amount"
|
||||||
|
/>
|
||||||
|
<p class="text-muted-foreground mt-1 text-xs">
|
||||||
|
Max bet: {MAX_BET_AMOUNT.toLocaleString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Percentage Quick Actions -->
|
||||||
|
<div>
|
||||||
|
<div class="grid grid-cols-4 gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onclick={() => setBetAmount(Math.floor(Math.min(balance, MAX_BET_AMOUNT) * 0.25))}
|
||||||
|
disabled={isPlaying}>25%</Button
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onclick={() => setBetAmount(Math.floor(Math.min(balance, MAX_BET_AMOUNT) * 0.5))}
|
||||||
|
disabled={isPlaying}>50%</Button
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onclick={() => setBetAmount(Math.floor(Math.min(balance, MAX_BET_AMOUNT) * 0.75))}
|
||||||
|
disabled={isPlaying}>75%</Button
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onclick={() => setBetAmount(Math.floor(Math.min(balance, MAX_BET_AMOUNT)))}
|
||||||
|
disabled={isPlaying}>Max</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
{#if !isPlaying}
|
||||||
|
<Button class="h-12 flex-1 text-lg" onclick={startGame} disabled={!canBet}>
|
||||||
|
Start Game
|
||||||
|
</Button>
|
||||||
|
{:else}
|
||||||
|
<!-- Auto Cashout Timer -->
|
||||||
|
{#if hasRevealedTile}
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="h-px w-full bg-border"></div>
|
||||||
|
<div class="text-center text-xs text-muted-foreground">
|
||||||
|
Auto Cashout in {Math.ceil(AUTO_CASHOUT_TIME - autoCashoutTimer)}s
|
||||||
|
</div>
|
||||||
|
<div class="h-1 w-full bg-muted rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
class="h-full bg-primary transition-all duration-100"
|
||||||
|
class:urgent={autoCashoutTimer >= 7}
|
||||||
|
style="width: {autoCashoutProgress}%"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="h-px w-full bg-border"></div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<Button class="h-12 flex-1 text-lg" onclick={cashOut} disabled={!isPlaying}>
|
||||||
|
{hasRevealedTile ? 'Cash Out' : 'Abort Bet'}
|
||||||
|
</Button>
|
||||||
|
<!-- Current Stats -->
|
||||||
|
{#if hasRevealedTile}
|
||||||
|
<div class="bg-muted/50 space-y-2 rounded-lg p-3">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>Current Profit:</span>
|
||||||
|
<span class="text-success">
|
||||||
|
+{formatValue(betAmount * (currentMultiplier - 1))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>Next Tile:</span>
|
||||||
|
<span>
|
||||||
|
+{formatValue(betAmount * (calculateRawMultiplier(revealedTiles.length + 1, mineCount) - 1))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span>Current Multiplier:</span>
|
||||||
|
<span>{currentMultiplier.toFixed(2)}x</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.mines-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
gap: 4px;
|
||||||
|
background: var(--muted);
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 8px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mines-grid.pulse-warning {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mines-grid.pulse-warning::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
right: -2px;
|
||||||
|
bottom: -2px;
|
||||||
|
left: -2px;
|
||||||
|
border: 2px solid var(--ring);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
animation: pulse 1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.urgent {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.urgent::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
right: -2px;
|
||||||
|
bottom: -2px;
|
||||||
|
left: -2px;
|
||||||
|
border: 2px solid var(--ring);
|
||||||
|
border-radius: 9999px;
|
||||||
|
animation: pulse 0.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-tile {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: calc(var(--radius) - 2px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-tile:hover:not(:disabled) {
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-tile.revealed {
|
||||||
|
background: var(--muted);
|
||||||
|
transform: rotateY(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-tile.mine {
|
||||||
|
background-color: rgba(239, 68, 68, 0.3);
|
||||||
|
border: 2px solid rgb(239, 68, 68);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-tile.mine img {
|
||||||
|
filter: brightness(0.9) contrast(1.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-tile.safe {
|
||||||
|
background-color: rgba(34, 197, 94, 0.2);
|
||||||
|
border: 2px solid rgb(34, 197, 94);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-tile img {
|
||||||
|
backface-visibility: hidden;
|
||||||
|
transform: rotateY(180deg);
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
98
website/src/lib/server/games/mines.ts
Normal file
98
website/src/lib/server/games/mines.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
import { db } from '$lib/server/db';
|
||||||
|
import { user } from '$lib/server/db/schema';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
|
||||||
|
|
||||||
|
interface MinesSession {
|
||||||
|
sessionToken: string;
|
||||||
|
betAmount: number;
|
||||||
|
mineCount: number;
|
||||||
|
minePositions: number[];
|
||||||
|
revealedTiles: number[];
|
||||||
|
startTime: number;
|
||||||
|
currentMultiplier: number;
|
||||||
|
status: 'active' | 'won' | 'lost';
|
||||||
|
lastActivity: number;
|
||||||
|
userId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const activeGames = new Map<string, MinesSession>();
|
||||||
|
|
||||||
|
// Clean up old games every minute.
|
||||||
|
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})`);
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export function calculateMultiplier(picks: number, mines: number, betAmount: number): number {
|
||||||
|
const TOTAL_TILES = 25;
|
||||||
|
const HOUSE_EDGE = 0.05;
|
||||||
|
|
||||||
|
let probability = 1;
|
||||||
|
for (let i = 0; i < picks; i++) {
|
||||||
|
probability *= (TOTAL_TILES - mines - i) / (TOTAL_TILES - i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (probability <= 0) return 1.0;
|
||||||
|
|
||||||
|
const fairMultiplier = (1 / probability) * (1 - HOUSE_EDGE);
|
||||||
|
const rawPayout = fairMultiplier * betAmount;
|
||||||
|
|
||||||
|
const maxPayout = getMaxPayout(betAmount, picks);
|
||||||
|
const cappedPayout = Math.min(rawPayout, maxPayout);
|
||||||
|
const effectiveMultiplier = cappedPayout / betAmount;
|
||||||
|
|
||||||
|
return Math.max(1.0, effectiveMultiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
74
website/src/routes/api/gambling/mines/cashout/+server.ts
Normal file
74
website/src/routes/api/gambling/mines/cashout/+server.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
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 { activeGames } 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 game = activeGames.get(sessionToken);
|
||||||
|
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. This could be changed later to keep the initial bet on the Server
|
||||||
|
if (game.revealedTiles.length === 0) {
|
||||||
|
payout = game.betAmount;
|
||||||
|
newBalance = Math.round((currentBalance + payout) * 100000000) / 100000000;
|
||||||
|
} else {
|
||||||
|
// Calculate payout
|
||||||
|
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));
|
||||||
|
|
||||||
|
activeGames.delete(sessionToken);
|
||||||
|
|
||||||
|
return {
|
||||||
|
newBalance,
|
||||||
|
payout,
|
||||||
|
amountWagered: game.betAmount,
|
||||||
|
isAbort: game.revealedTiles.length === 0
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
};
|
||||||
81
website/src/routes/api/gambling/mines/reveal/+server.ts
Normal file
81
website/src/routes/api/gambling/mines/reveal/+server.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { auth } from '$lib/auth';
|
||||||
|
import { error, json } from '@sveltejs/kit';
|
||||||
|
import { activeGames, 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';
|
||||||
|
|
||||||
|
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();
|
||||||
|
const game = activeGames.get(sessionToken);
|
||||||
|
|
||||||
|
if (!game) {
|
||||||
|
return json({ error: 'Invalid session' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (game.revealedTiles.includes(tileIndex)) {
|
||||||
|
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))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
activeGames.delete(sessionToken);
|
||||||
|
|
||||||
|
return json({
|
||||||
|
hitMine: true,
|
||||||
|
minePositions,
|
||||||
|
newBalance: Number(userData.baseCurrencyBalance),
|
||||||
|
status: 'lost'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Safe tile
|
||||||
|
game.revealedTiles.push(tileIndex);
|
||||||
|
game.currentMultiplier = calculateMultiplier(
|
||||||
|
game.revealedTiles.length,
|
||||||
|
game.mineCount,
|
||||||
|
game.betAmount
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if all safe tiles are revealed. Crazy when you get this :)
|
||||||
|
if (game.revealedTiles.length === 25 - game.mineCount) {
|
||||||
|
game.status = 'won';
|
||||||
|
}
|
||||||
|
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
};
|
||||||
100
website/src/routes/api/gambling/mines/start/+server.ts
Normal file
100
website/src/routes/api/gambling/mines/start/+server.ts
Normal file
|
|
@ -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 { activeGames } 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 || !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<number>();
|
||||||
|
while (positions.size < mineCount) {
|
||||||
|
positions.add(Math.floor(Math.random() * 25));
|
||||||
|
}
|
||||||
|
const safePositions = [];
|
||||||
|
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
|
||||||
|
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();
|
||||||
|
|
||||||
|
// Create session
|
||||||
|
activeGames.set(sessionToken, {
|
||||||
|
sessionToken,
|
||||||
|
betAmount: roundedAmount,
|
||||||
|
mineCount,
|
||||||
|
minePositions: Array.from(positions),
|
||||||
|
revealedTiles: [],
|
||||||
|
startTime: now,
|
||||||
|
lastActivity: now,
|
||||||
|
currentMultiplier: 1,
|
||||||
|
status: 'active',
|
||||||
|
userId
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hold bet amount on server to prevent the user from like sending it to another account and farming money without a risk
|
||||||
|
await tx
|
||||||
|
.update(user)
|
||||||
|
.set({
|
||||||
|
baseCurrencyBalance: (roundedBalance - roundedAmount).toFixed(8),
|
||||||
|
updatedAt: new Date()
|
||||||
|
})
|
||||||
|
.where(eq(user.id, userId));
|
||||||
|
|
||||||
|
return { sessionToken };
|
||||||
|
});
|
||||||
|
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Coinflip from '$lib/components/self/games/Coinflip.svelte';
|
import Coinflip from '$lib/components/self/games/Coinflip.svelte';
|
||||||
import Slots from '$lib/components/self/games/Slots.svelte';
|
import Slots from '$lib/components/self/games/Slots.svelte';
|
||||||
|
import Mines from '$lib/components/self/games/Mines.svelte';
|
||||||
import { USER_DATA } from '$lib/stores/user-data';
|
import { USER_DATA } from '$lib/stores/user-data';
|
||||||
import { PORTFOLIO_SUMMARY, fetchPortfolioSummary } from '$lib/stores/portfolio-data';
|
import { PORTFOLIO_SUMMARY, fetchPortfolioSummary } from '$lib/stores/portfolio-data';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
@ -39,14 +40,12 @@
|
||||||
|
|
||||||
<SEO
|
<SEO
|
||||||
title="Gambling - Rugplay"
|
title="Gambling - Rugplay"
|
||||||
description="Play virtual gambling games with simulated currency in Rugplay. Try coinflip and slots games using virtual money with no real-world value - purely for entertainment."
|
description="Play virtual gambling games with simulated currency in Rugplay. Try coinflip, slots, and mines games using virtual money with no real-world value - purely for entertainment."
|
||||||
keywords="virtual gambling simulation, coinflip game, slots game, virtual casino, simulated gambling, entertainment games"
|
keywords="virtual gambling simulation, coinflip game, slots game, mines game, virtual casino, simulated gambling, entertainment games"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SignInConfirmDialog bind:open={shouldSignIn} />
|
<SignInConfirmDialog bind:open={shouldSignIn} />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="container mx-auto max-w-4xl p-6">
|
<div class="container mx-auto max-w-4xl p-6">
|
||||||
<h1 class="mb-6 text-center text-3xl font-bold">Gambling</h1>
|
<h1 class="mb-6 text-center text-3xl font-bold">Gambling</h1>
|
||||||
|
|
||||||
|
|
@ -54,7 +53,7 @@
|
||||||
<div class="flex h-96 items-center justify-center">
|
<div class="flex h-96 items-center justify-center">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="text-muted-foreground mb-4 text-xl">Sign in to start gambling</div>
|
<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>
|
<p class="text-muted-foreground mb-4 text-sm">You need an account to gamble away your life savings</p>
|
||||||
<Button onclick={() => (shouldSignIn = true)}>Sign In</Button>
|
<Button onclick={() => (shouldSignIn = true)}>Sign In</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -73,6 +72,12 @@
|
||||||
>
|
>
|
||||||
Slots
|
Slots
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={activeGame === 'mines' ? 'default' : 'outline'}
|
||||||
|
onclick={() => (activeGame = 'mines')}
|
||||||
|
>
|
||||||
|
Mines
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Game Content -->
|
<!-- Game Content -->
|
||||||
|
|
@ -80,6 +85,8 @@
|
||||||
<Coinflip bind:balance onBalanceUpdate={handleBalanceUpdate} />
|
<Coinflip bind:balance onBalanceUpdate={handleBalanceUpdate} />
|
||||||
{:else if activeGame === 'slots'}
|
{:else if activeGame === 'slots'}
|
||||||
<Slots bind:balance onBalanceUpdate={handleBalanceUpdate} />
|
<Slots bind:balance onBalanceUpdate={handleBalanceUpdate} />
|
||||||
|
{:else if activeGame === 'mines'}
|
||||||
|
<Mines bind:balance onBalanceUpdate={handleBalanceUpdate} />
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Reference in a new issue