98 lines
3.1 KiB
TypeScript
98 lines
3.1 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
||
import { auth } from "@/auth";
|
||
import { prisma } from "@/lib/prisma";
|
||
import { generateServerSeed, deriveInt } from "@/lib/provably-fair";
|
||
import { creditWalletCredits, debitWalletCredits, INSUFFICIENT_CREDITS } from "@/lib/wallet-safety";
|
||
|
||
export const dynamic = "force-dynamic";
|
||
|
||
const SYMBOLS = ["🍒", "🍋", "🍊", "🍇", "💎", "7️⃣"];
|
||
const SYMBOL_WEIGHTS = [30, 25, 20, 15, 7, 3]; // out of 100
|
||
|
||
// Paytable: 3 of a kind multipliers
|
||
const PAYTABLE: Record<string, number> = {
|
||
"🍒": 2,
|
||
"🍋": 3,
|
||
"🍊": 5,
|
||
"🍇": 10,
|
||
"💎": 25,
|
||
"7️⃣": 50,
|
||
};
|
||
|
||
function weightedSymbol(roll: number): string {
|
||
let cumulative = 0;
|
||
for (let i = 0; i < SYMBOLS.length; i++) {
|
||
cumulative += SYMBOL_WEIGHTS[i];
|
||
if (roll < cumulative) return SYMBOLS[i];
|
||
}
|
||
return SYMBOLS[SYMBOLS.length - 1];
|
||
}
|
||
|
||
export async function POST(req: NextRequest) {
|
||
const session = await auth();
|
||
if (!session?.user?.id) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||
|
||
const body = await req.json();
|
||
const { wageBLW, clientSeed } = body as { wageBLW: number; clientSeed?: string };
|
||
|
||
if (!Number.isInteger(wageBLW) || wageBLW < 1) return NextResponse.json({ error: "Invalid wager" }, { status: 400 });
|
||
|
||
const serverSeed = generateServerSeed();
|
||
const cs = clientSeed ?? "default";
|
||
|
||
const reels = [
|
||
weightedSymbol(deriveInt(serverSeed, cs, 0, 0, 99)),
|
||
weightedSymbol(deriveInt(serverSeed, cs, 1, 0, 99)),
|
||
weightedSymbol(deriveInt(serverSeed, cs, 2, 0, 99)),
|
||
];
|
||
|
||
let multiplier = 0;
|
||
let outcome = "loss";
|
||
|
||
if (reels[0] === reels[1] && reels[1] === reels[2]) {
|
||
multiplier = PAYTABLE[reels[0]] ?? 2;
|
||
outcome = "win";
|
||
} else if (reels[0] === reels[1] || reels[1] === reels[2] || reels[0] === reels[2]) {
|
||
multiplier = 1.5;
|
||
outcome = "win";
|
||
}
|
||
|
||
const payout = outcome === "win" ? Math.floor(wageBLW * multiplier) : 0;
|
||
|
||
try {
|
||
await prisma.$transaction(async (tx) => {
|
||
await debitWalletCredits(tx, session.user.id, wageBLW);
|
||
await tx.ledgerEntry.create({
|
||
data: { userId: session.user.id, delta: -wageBLW, type: "DEBIT_GAME_BET", memo: "SLOTS bet" },
|
||
});
|
||
if (payout > 0) {
|
||
await creditWalletCredits(tx, session.user.id, payout);
|
||
await tx.ledgerEntry.create({
|
||
data: { userId: session.user.id, delta: payout, type: "CREDIT_GAME_WIN", memo: "SLOTS payout" },
|
||
});
|
||
}
|
||
await tx.gameSession.create({
|
||
data: {
|
||
userId: session.user.id,
|
||
gameType: "SLOTS",
|
||
wageredBLW: wageBLW,
|
||
payoutBLW: payout,
|
||
multiplier,
|
||
outcome,
|
||
serverSeed,
|
||
clientSeed: cs,
|
||
resultData: { reels, multiplier },
|
||
},
|
||
});
|
||
});
|
||
} catch (e) {
|
||
if (e instanceof Error && e.message === INSUFFICIENT_CREDITS) {
|
||
return NextResponse.json({ error: "Insufficient balance" }, { status: 402 });
|
||
}
|
||
console.error(e);
|
||
return NextResponse.json({ error: "Could not settle slots game" }, { status: 500 });
|
||
}
|
||
|
||
return NextResponse.json({ reels, multiplier, payout, outcome, serverSeed });
|
||
}
|