clean mines component

This commit is contained in:
Face 2025-06-24 12:46:38 +03:00
parent 5533669745
commit ad1739f7f4

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input'; import { Input } from '$lib/components/ui/input';
import { import {
Card, Card,
CardContent, CardContent,
@ -9,41 +9,19 @@
CardHeader, CardHeader,
CardTitle CardTitle
} from '$lib/components/ui/card'; } 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 confetti from 'canvas-confetti';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { formatValue, playSound, showConfetti, showSchoolPrideCannons } from '$lib/utils'; import { formatValue, playSound, showConfetti, showSchoolPrideCannons } from '$lib/utils';
import { volumeSettings } from '$lib/stores/volume-settings'; import { volumeSettings } from '$lib/stores/volume-settings';
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import { ModeWatcher } from 'mode-watcher'; import { ModeWatcher } from 'mode-watcher';
import { Info } from 'lucide-svelte';
import { fetchPortfolioSummary } from '$lib/stores/portfolio-data'; import { fetchPortfolioSummary } from '$lib/stores/portfolio-data';
interface MinesResult {
won: boolean;
newBalance: number;
payout: number;
amountWagered: number;
sessionToken: string;
}
const GRID_SIZE = 5; const GRID_SIZE = 5;
const TOTAL_TILES = GRID_SIZE * GRID_SIZE; const TOTAL_TILES = GRID_SIZE * GRID_SIZE;
const MAX_BET_AMOUNT = 1000000; const MAX_BET_AMOUNT = 1000000;
const MIN_MINES = 3; const MIN_MINES = 3;
const AUTO_CASHOUT_TIME = 15; // 15 seconds total (10s game + 5s buffer) const AUTO_CASHOUT_TIME = 15;
let { let {
balance = $bindable(), balance = $bindable(),
@ -60,7 +38,6 @@
let revealedTiles = $state<number[]>([]); let revealedTiles = $state<number[]>([]);
let minePositions = $state<number[]>([]); let minePositions = $state<number[]>([]);
let currentMultiplier = $state(1); let currentMultiplier = $state(1);
let lastResult = $state<MinesResult | null>(null);
let autoCashoutTimer = $state(0); let autoCashoutTimer = $state(0);
let autoCashoutProgress = $state(0); let autoCashoutProgress = $state(0);
let sessionToken = $state<string | null>(null); let sessionToken = $state<string | null>(null);
@ -87,27 +64,23 @@
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);
} }
return probability === 0 ? 1.0 : Math.max(1.0, 1 / probability);
if (probability === 0) return 1.0;
return Math.max(1.0, 1 / probability);
} }
function setBetAmount(amount: number) { function setBetAmount(amount: number) {
const clampedAmount = Math.min(amount, Math.min(balance, MAX_BET_AMOUNT)); const clamped = Math.min(amount, Math.min(balance, MAX_BET_AMOUNT));
if (clampedAmount >= 0) { if (clamped >= 0) {
betAmount = clampedAmount; betAmount = clamped;
betAmountDisplay = clampedAmount.toLocaleString(); betAmountDisplay = clamped.toLocaleString();
} }
} }
function handleBetAmountInput(event: Event) { function handleBetAmountInput(event: Event) {
const target = event.target as HTMLInputElement; const value = (event.target as HTMLInputElement).value.replace(/,/g, '');
const value = target.value.replace(/,/g, ''); const num = parseFloat(value) || 0;
const numValue = parseFloat(value) || 0; const clamped = Math.min(num, Math.min(balance, MAX_BET_AMOUNT));
const clampedValue = Math.min(numValue, Math.min(balance, MAX_BET_AMOUNT)); betAmount = clamped;
betAmountDisplay = value;
betAmount = clampedValue;
betAmountDisplay = target.value;
} }
function handleBetAmountBlur() { function handleBetAmountBlur() {
@ -115,16 +88,13 @@
} }
function resetAutoCashoutTimer() { function resetAutoCashoutTimer() {
if (autoCashoutInterval) { if (autoCashoutInterval) clearInterval(autoCashoutInterval);
clearInterval(autoCashoutInterval);
}
autoCashoutTimer = 0; autoCashoutTimer = 0;
autoCashoutProgress = 0; autoCashoutProgress = 0;
} }
function startAutoCashoutTimer() { function startAutoCashoutTimer() {
if (!hasRevealedTile) return; if (!hasRevealedTile) return;
resetAutoCashoutTimer(); resetAutoCashoutTimer();
autoCashoutInterval = setInterval(() => { autoCashoutInterval = setInterval(() => {
if (autoCashoutTimer < AUTO_CASHOUT_TIME) { if (autoCashoutTimer < AUTO_CASHOUT_TIME) {
@ -141,46 +111,33 @@
async function handleTileClick(index: number) { async function handleTileClick(index: number) {
if (!isPlaying || revealedTiles.includes(index) || !sessionToken) return; if (!isPlaying || revealedTiles.includes(index) || !sessionToken) return;
lastClickedTile = index; lastClickedTile = index;
try { try {
const response = await fetch('/api/gambling/mines/reveal', { const response = await fetch('/api/gambling/mines/reveal', {
method: 'POST', method: 'POST',
headers: { headers: { 'Content-Type': 'application/json' },
'Content-Type': 'application/json' body: JSON.stringify({ sessionToken, tileIndex: index })
},
body: JSON.stringify({
sessionToken,
tileIndex: index
})
}); });
if (!response.ok) { if (!response.ok) {
const errorData = await response.json(); const errorData = await response.json();
throw new Error(errorData.error || 'Failed to reveal tile'); throw new Error(errorData.error || 'Failed to reveal tile');
} }
const result = await response.json(); const result = await response.json();
if (result.hitMine) { if (result.hitMine) {
playSound('lose'); playSound('lose');
revealedTiles = [...revealedTiles, index]; revealedTiles = [...revealedTiles, index];
minePositions = result.minePositions; minePositions = result.minePositions;
isPlaying = false; isPlaying = false;
resetAutoCashoutTimer(); resetAutoCashoutTimer();
balance = result.newBalance; balance = result.newBalance;
onBalanceUpdate?.(result.newBalance); onBalanceUpdate?.(result.newBalance);
} } else {
else {
playSound('flip'); playSound('flip');
revealedTiles = [...revealedTiles, index]; revealedTiles = [...revealedTiles, index];
clickedSafeTiles = [...clickedSafeTiles, index]; clickedSafeTiles = [...clickedSafeTiles, index];
currentMultiplier = result.currentMultiplier; currentMultiplier = result.currentMultiplier;
hasRevealedTile = true; hasRevealedTile = true;
startAutoCashoutTimer(); startAutoCashoutTimer();
if (result.status === 'won') { if (result.status === 'won') {
showSchoolPrideCannons(confetti); showSchoolPrideCannons(confetti);
showConfetti(confetti); showConfetti(confetti);
@ -197,18 +154,12 @@
async function cashOut() { async function cashOut() {
if (!isPlaying || !sessionToken) return; if (!isPlaying || !sessionToken) return;
try { try {
const response = await fetch('/api/gambling/mines/cashout', { const response = await fetch('/api/gambling/mines/cashout', {
method: 'POST', method: 'POST',
headers: { headers: { 'Content-Type': 'application/json' },
'Content-Type': 'application/json' body: JSON.stringify({ sessionToken })
},
body: JSON.stringify({
sessionToken
})
}); });
if (!response.ok) { if (!response.ok) {
const errorData = await response.json(); const errorData = await response.json();
if (!isAutoCashout || errorData.error !== 'Invalid session') { if (!isAutoCashout || errorData.error !== 'Invalid session') {
@ -216,15 +167,10 @@
} }
return; return;
} }
const result = await response.json(); const result = await response.json();
balance = result.newBalance; balance = result.newBalance;
onBalanceUpdate?.(balance); onBalanceUpdate?.(balance);
if (result.payout > betAmount) showConfetti(confetti);
if (result.payout > betAmount) {
showConfetti(confetti);
}
playSound(result.isAbort ? 'flip' : 'win'); playSound(result.isAbort ? 'flip' : 'win');
isPlaying = false; isPlaying = false;
hasRevealedTile = false; hasRevealedTile = false;
@ -241,33 +187,23 @@
async function startGame() { async function startGame() {
if (!canBet) return; if (!canBet) return;
balance -= betAmount; balance -= betAmount;
onBalanceUpdate?.(balance); onBalanceUpdate?.(balance);
try { try {
const response = await fetch('/api/gambling/mines/start', { const response = await fetch('/api/gambling/mines/start', {
method: 'POST', method: 'POST',
headers: { headers: { 'Content-Type': 'application/json' },
'Content-Type': 'application/json' body: JSON.stringify({ betAmount, mineCount })
},
body: JSON.stringify({
betAmount,
mineCount
})
}); });
if (!response.ok) { if (!response.ok) {
const errorData = await response.json(); const errorData = await response.json();
balance += betAmount; balance += betAmount;
onBalanceUpdate?.(balance); onBalanceUpdate?.(balance);
throw new Error(errorData.error || 'Failed to start game'); throw new Error(errorData.error || 'Failed to start game');
} }
const result = await response.json(); const result = await response.json();
isPlaying = true; isPlaying = true;
hasRevealedTile = false; hasRevealedTile = false;
lastResult = null;
revealedTiles = []; revealedTiles = [];
clickedSafeTiles = []; clickedSafeTiles = [];
currentMultiplier = 1; currentMultiplier = 1;
@ -281,10 +217,8 @@
} }
} }
// Dynmaically fetch the correct balance.
onMount(async () => { onMount(async () => {
volumeSettings.load(); volumeSettings.load();
try { try {
const data = await fetchPortfolioSummary(); const data = await fetchPortfolioSummary();
if (data) { if (data) {
@ -296,9 +230,7 @@
} }
}); });
onDestroy(() => { onDestroy(resetAutoCashoutTimer);
resetAutoCashoutTimer();
});
</script> </script>
<Card> <Card>
@ -306,7 +238,6 @@
<CardTitle>Mines</CardTitle> <CardTitle>Mines</CardTitle>
<CardDescription> <CardDescription>
Navigate through the minefield and cash out before hitting a mine! Navigate through the minefield and cash out before hitting a mine!
<!-- Info popup and button removed -->
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@ -325,20 +256,21 @@
<ModeWatcher /> <ModeWatcher />
<button <button
class="mine-tile" class="mine-tile"
class:revealed={revealedTiles.includes(index)}
class:mine={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')}
onclick={() => handleTileClick(index)} onclick={() => handleTileClick(index)}
disabled={!isPlaying} disabled={!isPlaying}
class:revealed={revealedTiles.includes(index)}
class:mine={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')}
aria-label="Tile"
> >
{#if revealedTiles.includes(index)} {#if revealedTiles.includes(index)}
{#if minePositions.includes(index)} {#if minePositions.includes(index)}
<img <img src="/facedev/avif/bussin.avif" alt="Mine" class="h-8 w-8 object-contain" />
src="/facedev/avif/bussin.avif"
alt="Mine"
class="h-8 w-8 object-contain"
/>
{:else} {:else}
<img <img
src="/facedev/avif/twoblade.avif" src="/facedev/avif/twoblade.avif"
@ -351,27 +283,25 @@
{/each} {/each}
</div> </div>
</div> </div>
<!-- Right Side: Controls -->
<!-- Right Side: Controls Things -->
<div class="space-y-4"> <div class="space-y-4">
<!-- 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="mb-2 block text-sm font-medium">Number of Mines</label>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Button <Button
variant="secondary" variant="secondary"
size="sm" size="sm"
onclick={() => mineCount = Math.max(mineCount - 1, MIN_MINES)} onclick={() => (mineCount = Math.max(mineCount - 1, MIN_MINES))}
disabled={isPlaying || mineCount <= MIN_MINES} disabled={isPlaying || mineCount <= MIN_MINES}
aria-label="Decrease mines" aria-label="Decrease mines">-</Button
>-</Button> >
<Input <Input
id="mine-count" id="mine-count"
type="number" type="number"
min={MIN_MINES} min={MIN_MINES}
max={24} max={24}
value={mineCount} value={mineCount}
oninput={e => { oninput={(e) => {
const target = e.target as HTMLInputElement | null; const target = e.target as HTMLInputElement | null;
const val = Math.max( const val = Math.max(
MIN_MINES, MIN_MINES,
@ -385,30 +315,24 @@
<Button <Button
variant="secondary" variant="secondary"
size="sm" size="sm"
onclick={() => mineCount = Math.min(mineCount + 1, 24)} onclick={() => (mineCount = Math.min(mineCount + 1, 24))}
disabled={isPlaying || mineCount >= 24} disabled={isPlaying || mineCount >= 24}
aria-label="Increase mines" aria-label="Increase mines">+</Button
>+</Button> >
</div> </div>
<p class="text-muted-foreground mt-1 text-xs"> <p class="text-muted-foreground mt-1 text-xs">
You will get You will get
<span class="font-semibold text-success"> <span class="text-success font-semibold">
{(calculateRawMultiplier( {calculateRawMultiplier(isPlaying ? revealedTiles.length + 1 : 1, mineCount).toFixed(
isPlaying ? revealedTiles.length + 1 : 1, 2
mineCount )}x
)).toFixed(2)}x
</span> </span>
per tile, probability of winning: per tile, probability of winning:
<span class="font-semibold text-success"> <span class="text-success font-semibold">
{calculateProbability( {calculateProbability(isPlaying ? 1 : 1, mineCount)}%
isPlaying ? 1 : 1,
mineCount
)}%
</span> </span>
</p> </p>
</div> </div>
<!-- Bet Amount -->
<div> <div>
<label for="bet-amount" class="mb-2 block text-sm font-medium">Bet Amount</label> <label for="bet-amount" class="mb-2 block text-sm font-medium">Bet Amount</label>
<Input <Input
@ -424,8 +348,6 @@
Max bet: {MAX_BET_AMOUNT.toLocaleString()} Max bet: {MAX_BET_AMOUNT.toLocaleString()}
</p> </p>
</div> </div>
<!-- Percentage Quick Actions -->
<div> <div>
<div class="grid grid-cols-4 gap-2"> <div class="grid grid-cols-4 gap-2">
<Button <Button
@ -450,32 +372,30 @@
size="sm" size="sm"
variant="outline" variant="outline"
onclick={() => setBetAmount(Math.floor(Math.min(balance, MAX_BET_AMOUNT)))} onclick={() => setBetAmount(Math.floor(Math.min(balance, MAX_BET_AMOUNT)))}
disabled={isPlaying}>Max</Button> disabled={isPlaying}>Max</Button
>
</div> </div>
</div> </div>
<!-- Action Buttons -->
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
{#if !isPlaying} {#if !isPlaying}
<Button class="h-12 flex-1 text-lg" onclick={startGame} disabled={!canBet}> <Button class="h-12 flex-1 text-lg" onclick={startGame} disabled={!canBet}>
Start Game Start Game
</Button> </Button>
{:else} {:else}
<!-- Auto Cashout Timer -->
{#if hasRevealedTile} {#if hasRevealedTile}
<div class="space-y-1"> <div class="space-y-1">
<div class="h-px w-full bg-border"></div> <div class="bg-border h-px w-full"></div>
<div class="text-center text-xs text-muted-foreground"> <div class="text-muted-foreground text-center text-xs">
Auto Cashout in {Math.ceil(AUTO_CASHOUT_TIME - autoCashoutTimer)}s Auto Cashout in {Math.ceil(AUTO_CASHOUT_TIME - autoCashoutTimer)}s
</div> </div>
<div class="h-1 w-full bg-muted rounded-full overflow-hidden"> <div class="bg-muted h-1 w-full overflow-hidden rounded-full">
<div <div
class="h-full bg-primary transition-all duration-100" class="bg-primary h-full transition-all duration-100"
class:urgent={autoCashoutTimer >= 7} class:urgent={autoCashoutTimer >= 7}
style="width: {autoCashoutProgress}%" style="width: {autoCashoutProgress}%"
></div> ></div>
</div> </div>
<div class="h-px w-full bg-border"></div> <div class="bg-border h-px w-full"></div>
</div> </div>
{/if} {/if}
<Button class="h-12 flex-1 text-lg" onclick={cashOut} disabled={!isPlaying}> <Button class="h-12 flex-1 text-lg" onclick={cashOut} disabled={!isPlaying}>
@ -493,7 +413,9 @@
<div class="flex justify-between"> <div class="flex justify-between">
<span>Next Tile:</span> <span>Next Tile:</span>
<span> <span>
+{formatValue(betAmount * (calculateRawMultiplier(revealedTiles.length + 1, mineCount) - 1))} +{formatValue(
betAmount * (calculateRawMultiplier(revealedTiles.length + 1, mineCount) - 1)
)}
</span> </span>
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
@ -609,4 +531,4 @@
height: 32px; height: 32px;
object-fit: contain; object-fit: contain;
} }
</style> </style>