mirror of
https://github.com/MrUnknownDE/bierkasten-casino.git
synced 2026-05-02 03:46:06 +02:00
add free spins
This commit is contained in:
+113
-37
@@ -1,7 +1,6 @@
|
||||
// backend/src/routes/slot.ts
|
||||
import { Router } from "express";
|
||||
import { pool } from "../db";
|
||||
import { spinBookOfBier } from "../services/slotService";
|
||||
import { spinBookOfBier, getFreeSpinsForBooks } from "../services/slotService";
|
||||
|
||||
export const slotRouter = Router();
|
||||
|
||||
@@ -16,18 +15,9 @@ function requireAuth(req: any, res: any, next: any) {
|
||||
// POST /slot/book-of-bier/spin
|
||||
slotRouter.post("/book-of-bier/spin", requireAuth, async (req: any, res) => {
|
||||
const userId = req.session.userId as number;
|
||||
// --- KORREKTUR: Falscher Parametername ---
|
||||
// Das Frontend sendet `bet_amount`, nicht `bet`.
|
||||
|
||||
const betRaw = req.body?.bet_amount;
|
||||
|
||||
const bet = parseInt(betRaw, 10);
|
||||
if (!Number.isFinite(bet) || bet <= 0) {
|
||||
return res.status(400).json({ error: "Invalid bet amount" });
|
||||
}
|
||||
|
||||
if (bet > 1000) {
|
||||
return res.status(400).json({ error: "Bet too high (max 1000 Bierkästen)" });
|
||||
}
|
||||
const requestedBet = parseInt(betRaw, 10);
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
@@ -36,9 +26,15 @@ slotRouter.post("/book-of-bier/spin", requireAuth, async (req: any, res) => {
|
||||
const walletRes = await client.query<{
|
||||
user_id: number;
|
||||
balance: number;
|
||||
free_spins_bob_remaining: number;
|
||||
free_spins_bob_bet: number | null;
|
||||
}>(
|
||||
`
|
||||
SELECT user_id, balance
|
||||
SELECT
|
||||
user_id,
|
||||
balance,
|
||||
free_spins_bob_remaining,
|
||||
free_spins_bob_bet
|
||||
FROM wallets
|
||||
WHERE user_id = $1
|
||||
FOR UPDATE
|
||||
@@ -53,39 +49,108 @@ slotRouter.post("/book-of-bier/spin", requireAuth, async (req: any, res) => {
|
||||
|
||||
const wallet = walletRes.rows[0];
|
||||
|
||||
if (wallet.balance < bet) {
|
||||
await client.query("ROLLBACK");
|
||||
return res.status(400).json({ error: "Nicht genug Bierkästen für diese Wette" });
|
||||
const hasFreeSpins = (wallet.free_spins_bob_remaining || 0) > 0;
|
||||
|
||||
let isFreeSpin = false;
|
||||
let effectiveBet: number;
|
||||
|
||||
if (hasFreeSpins) {
|
||||
isFreeSpin = true;
|
||||
// Falls aus irgendeinem Grund kein Bet gespeichert ist, fallback auf 10
|
||||
effectiveBet =
|
||||
wallet.free_spins_bob_bet && wallet.free_spins_bob_bet > 0
|
||||
? wallet.free_spins_bob_bet
|
||||
: Number.isFinite(requestedBet) && requestedBet > 0
|
||||
? requestedBet
|
||||
: 10;
|
||||
} else {
|
||||
if (!Number.isFinite(requestedBet) || requestedBet <= 0) {
|
||||
await client.query("ROLLBACK");
|
||||
return res.status(400).json({ error: "Invalid bet amount" });
|
||||
}
|
||||
|
||||
if (requestedBet > 1000) {
|
||||
await client.query("ROLLBACK");
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "Bet too high (max 1000 Bierkästen)" });
|
||||
}
|
||||
|
||||
if (wallet.balance < requestedBet) {
|
||||
await client.query("ROLLBACK");
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "Nicht genug Bierkästen für diese Wette" });
|
||||
}
|
||||
|
||||
effectiveBet = requestedBet;
|
||||
}
|
||||
|
||||
// Spin ausführen (reine Logik)
|
||||
const spin = spinBookOfBier(bet);
|
||||
const spin = spinBookOfBier(effectiveBet);
|
||||
const winAmount = spin.totalWin;
|
||||
|
||||
const newBalance = wallet.balance - bet + winAmount;
|
||||
let newBalance = wallet.balance;
|
||||
|
||||
if (isFreeSpin) {
|
||||
// Bei Freispielen kein Abzug, nur Gewinne drauf
|
||||
newBalance = wallet.balance + winAmount;
|
||||
} else {
|
||||
// Normales Spiel: Einsatz abziehen, Gewinn addieren
|
||||
newBalance = wallet.balance - effectiveBet + winAmount;
|
||||
}
|
||||
|
||||
// Free-Spin-Zustand aktualisieren
|
||||
let newFreeSpinsRemaining = wallet.free_spins_bob_remaining || 0;
|
||||
let newFreeSpinsBet = wallet.free_spins_bob_bet;
|
||||
let freeSpinsAwardedThisSpin = 0;
|
||||
|
||||
if (isFreeSpin) {
|
||||
newFreeSpinsRemaining = Math.max(0, newFreeSpinsRemaining - 1);
|
||||
if (newFreeSpinsRemaining === 0) {
|
||||
newFreeSpinsBet = null;
|
||||
}
|
||||
// In Freispielen selbst werden KEINE neuen Freispiele vergeben,
|
||||
// um unendliche Ketten zu vermeiden.
|
||||
} else {
|
||||
// Normales Spiel: hier können Freispiele frisch gewonnen werden
|
||||
const fs = getFreeSpinsForBooks(spin.bookCount);
|
||||
if (fs > 0) {
|
||||
freeSpinsAwardedThisSpin = fs;
|
||||
newFreeSpinsRemaining = fs;
|
||||
newFreeSpinsBet = effectiveBet;
|
||||
}
|
||||
}
|
||||
|
||||
// Wallet aktualisieren
|
||||
const updatedWallet = await client.query<{
|
||||
user_id: number;
|
||||
balance: number;
|
||||
free_spins_bob_remaining: number;
|
||||
free_spins_bob_bet: number | null;
|
||||
}>(
|
||||
`
|
||||
UPDATE wallets
|
||||
SET balance = $2
|
||||
SET balance = $2,
|
||||
free_spins_bob_remaining = $3,
|
||||
free_spins_bob_bet = $4
|
||||
WHERE user_id = $1
|
||||
RETURNING user_id, balance
|
||||
RETURNING user_id, balance, free_spins_bob_remaining, free_spins_bob_bet
|
||||
`,
|
||||
[userId, newBalance]
|
||||
[userId, newBalance, newFreeSpinsRemaining, newFreeSpinsBet]
|
||||
);
|
||||
|
||||
// Transaktionen loggen
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO wallet_transactions (user_id, amount, reason)
|
||||
VALUES ($1, $2, $3)
|
||||
`,
|
||||
[userId, -bet, "slot_bet:book_of_bier"]
|
||||
);
|
||||
if (!isFreeSpin) {
|
||||
// Einsatz
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO wallet_transactions (user_id, amount, reason)
|
||||
VALUES ($1, $2, $3)
|
||||
`,
|
||||
[userId, -effectiveBet, "slot_bet:book_of_bier"]
|
||||
);
|
||||
}
|
||||
|
||||
if (winAmount > 0) {
|
||||
await client.query(
|
||||
@@ -93,35 +158,46 @@ slotRouter.post("/book-of-bier/spin", requireAuth, async (req: any, res) => {
|
||||
INSERT INTO wallet_transactions (user_id, amount, reason)
|
||||
VALUES ($1, $2, $3)
|
||||
`,
|
||||
[userId, winAmount, "slot_win:book_of_bier"]
|
||||
[
|
||||
userId,
|
||||
winAmount,
|
||||
isFreeSpin ? "slot_win_free:book_of_bier" : "slot_win:book_of_bier"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Slot-Runde loggen
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO slot_rounds (user_id, game_name, bet_amount, win_amount, book_count, grid)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
INSERT INTO slot_rounds (user_id, game_name, bet_amount, win_amount, book_count, grid, is_free_spin)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
`,
|
||||
[
|
||||
userId,
|
||||
"book_of_bier",
|
||||
bet,
|
||||
effectiveBet,
|
||||
winAmount,
|
||||
spin.bookCount,
|
||||
JSON.stringify(spin.grid)
|
||||
JSON.stringify(spin.grid),
|
||||
isFreeSpin
|
||||
]
|
||||
);
|
||||
|
||||
await client.query("COMMIT");
|
||||
|
||||
const walletRow = updatedWallet.rows[0];
|
||||
|
||||
res.json({
|
||||
bet_amount: bet,
|
||||
bet_amount: effectiveBet,
|
||||
win_amount: winAmount,
|
||||
balance_after: updatedWallet.rows[0].balance,
|
||||
balance_after: walletRow.balance,
|
||||
book_count: spin.bookCount,
|
||||
grid: spin.grid,
|
||||
line_wins: spin.lineWins
|
||||
line_wins: spin.lineWins,
|
||||
is_free_spin: isFreeSpin,
|
||||
free_spins_remaining: walletRow.free_spins_bob_remaining,
|
||||
free_spins_awarded: freeSpinsAwardedThisSpin,
|
||||
free_spins_bet_amount: walletRow.free_spins_bob_bet
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("POST /slot/book-of-bier/spin error:", err);
|
||||
|
||||
@@ -23,7 +23,9 @@ walletRouter.get("/", requireAuth, async (req: any, res) => {
|
||||
user_id: wallet.user_id,
|
||||
balance: wallet.balance,
|
||||
last_claim_at: wallet.last_claim_at,
|
||||
next_claim_in_ms: nextClaimInMs
|
||||
next_claim_in_ms: nextClaimInMs,
|
||||
free_spins_bob_remaining: wallet.free_spins_bob_remaining,
|
||||
free_spins_bob_bet: wallet.free_spins_bob_bet
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("GET /wallet error:", err);
|
||||
@@ -42,10 +44,12 @@ walletRouter.post("/claim", requireAuth, async (req: any, res) => {
|
||||
balance: result.wallet.balance,
|
||||
last_claim_at: result.wallet.last_claim_at,
|
||||
claimed_amount: result.claimedAmount,
|
||||
next_claim_in_ms: result.nextClaimInMs
|
||||
next_claim_in_ms: result.nextClaimInMs,
|
||||
free_spins_bob_remaining: result.wallet.free_spins_bob_remaining,
|
||||
free_spins_bob_bet: result.wallet.free_spins_bob_bet
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("POST /wallet/claim error:", err);
|
||||
res.status(500).json({ error: "Failed to claim beer crates" });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,6 @@
|
||||
// path: backend/src/services/slotService.ts
|
||||
import { randomInt } from "crypto";
|
||||
|
||||
// backend/src/services/slotService.ts
|
||||
export type SymbolId =
|
||||
| "TEN" | "J" | "Q" | "K" | "A"
|
||||
@@ -39,6 +42,17 @@ const BOOK_SCATTER: { [count: number]: number } = {
|
||||
5: 20
|
||||
};
|
||||
|
||||
// Wie viele Freispiele gibt es bei x Büchern im Hauptspiel?
|
||||
// 3 Bücher -> 10 Freispiele
|
||||
// 4 Bücher -> 12 Freispiele
|
||||
// 5+ Bücher -> 15 Freispiele
|
||||
export function getFreeSpinsForBooks(bookCount: number): number {
|
||||
if (bookCount >= 5) return 15;
|
||||
if (bookCount === 4) return 12;
|
||||
if (bookCount === 3) return 10;
|
||||
return 0;
|
||||
}
|
||||
|
||||
export interface LineWin {
|
||||
lineIndex: number;
|
||||
symbol: SymbolId;
|
||||
@@ -173,4 +187,4 @@ export function spinBookOfBier(bet: number): SpinResult {
|
||||
lineWins,
|
||||
bookCount: books
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -5,19 +5,26 @@ export interface Wallet {
|
||||
user_id: number;
|
||||
balance: number;
|
||||
last_claim_at: string | null;
|
||||
|
||||
free_spins_bob_remaining: number;
|
||||
free_spins_bob_bet: number | null;
|
||||
}
|
||||
|
||||
const HOURLY_RATE = 25;
|
||||
const CLAIM_INTERVAL_MS = 60 * 60 * 1000; // 1 Stunde
|
||||
|
||||
// Begrenzung: maximal so viele Stunden werden nachträglich gutgeschrieben.
|
||||
// Beispiel: 24 => max 24 * 25 = 600 Bierkästen pro Claim.
|
||||
const MAX_OFFLINE_HOURS = 24;
|
||||
// Harte Obergrenze: Pro Claim maximal so viele Bierkästen gutschreiben.
|
||||
const MAX_CLAIM_PER_CLAIM = 500;
|
||||
|
||||
export async function getWalletForUser(userId: number): Promise<Wallet> {
|
||||
const rows = await query<Wallet>(
|
||||
`
|
||||
SELECT user_id, balance, last_claim_at
|
||||
SELECT
|
||||
user_id,
|
||||
balance,
|
||||
last_claim_at,
|
||||
free_spins_bob_remaining,
|
||||
free_spins_bob_bet
|
||||
FROM wallets
|
||||
WHERE user_id = $1
|
||||
`,
|
||||
@@ -28,9 +35,9 @@ export async function getWalletForUser(userId: number): Promise<Wallet> {
|
||||
// Fallback, falls aus irgendeinem Grund noch kein Wallet existiert
|
||||
const created = await query<Wallet>(
|
||||
`
|
||||
INSERT INTO wallets (user_id, balance, last_claim_at)
|
||||
VALUES ($1, 0, NULL)
|
||||
RETURNING user_id, balance, last_claim_at
|
||||
INSERT INTO wallets (user_id, balance, last_claim_at, free_spins_bob_remaining, free_spins_bob_bet)
|
||||
VALUES ($1, 0, NULL, 0, NULL)
|
||||
RETURNING user_id, balance, last_claim_at, free_spins_bob_remaining, free_spins_bob_bet
|
||||
`,
|
||||
[userId]
|
||||
);
|
||||
@@ -46,6 +53,15 @@ export interface ClaimResult {
|
||||
nextClaimInMs: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Claim-Logik:
|
||||
* - Erste Claim: einmalig HOURLY_RATE.
|
||||
* - Danach: wenn seit last_claim_at >= 1h vergangen ist,
|
||||
* werden die vollen "nachholbaren" Stunden berechnet,
|
||||
* aber pro Claim maximal MAX_CLAIM_PER_CLAIM gutgeschrieben.
|
||||
* - Egal wie lange jemand AFK war -> pro Klick maximal MAX_CLAIM_PER_CLAIM.
|
||||
* - Nach einem erfolgreichen Claim wird last_claim_at auf "jetzt" gesetzt.
|
||||
*/
|
||||
export async function claimHourlyForUser(userId: number): Promise<ClaimResult> {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
@@ -53,7 +69,12 @@ export async function claimHourlyForUser(userId: number): Promise<ClaimResult> {
|
||||
|
||||
const res = await client.query<Wallet>(
|
||||
`
|
||||
SELECT user_id, balance, last_claim_at
|
||||
SELECT
|
||||
user_id,
|
||||
balance,
|
||||
last_claim_at,
|
||||
free_spins_bob_remaining,
|
||||
free_spins_bob_bet
|
||||
FROM wallets
|
||||
WHERE user_id = $1
|
||||
FOR UPDATE
|
||||
@@ -65,9 +86,9 @@ export async function claimHourlyForUser(userId: number): Promise<ClaimResult> {
|
||||
if (res.rows.length === 0) {
|
||||
const inserted = await client.query<Wallet>(
|
||||
`
|
||||
INSERT INTO wallets (user_id, balance, last_claim_at)
|
||||
VALUES ($1, 0, NULL)
|
||||
RETURNING user_id, balance, last_claim_at
|
||||
INSERT INTO wallets (user_id, balance, last_claim_at, free_spins_bob_remaining, free_spins_bob_bet)
|
||||
VALUES ($1, 0, NULL, 0, NULL)
|
||||
RETURNING user_id, balance, last_claim_at, free_spins_bob_remaining, free_spins_bob_bet
|
||||
`,
|
||||
[userId]
|
||||
);
|
||||
@@ -83,19 +104,16 @@ export async function claimHourlyForUser(userId: number): Promise<ClaimResult> {
|
||||
let nextClaimInMs = 0;
|
||||
|
||||
if (!lastClaim) {
|
||||
// Erste Claim: direkt 25 geben
|
||||
claimedAmount = HOURLY_RATE;
|
||||
nextClaimInMs = CLAIM_INTERVAL_MS;
|
||||
} else {
|
||||
const diffMs = now.getTime() - lastClaim.getTime();
|
||||
|
||||
if (diffMs >= CLAIM_INTERVAL_MS) {
|
||||
const rawIntervals = Math.floor(diffMs / CLAIM_INTERVAL_MS);
|
||||
// Begrenzung, um Unreal-Sprünge (75k etc.) zu verhindern
|
||||
const effectiveIntervals = Math.min(rawIntervals, MAX_OFFLINE_HOURS);
|
||||
const rawClaim = rawIntervals * HOURLY_RATE;
|
||||
|
||||
claimedAmount = effectiveIntervals * HOURLY_RATE;
|
||||
|
||||
// Nach einem erfolgreichen Claim: nächster in 1h
|
||||
claimedAmount = Math.min(rawClaim, MAX_CLAIM_PER_CLAIM);
|
||||
nextClaimInMs = CLAIM_INTERVAL_MS;
|
||||
} else {
|
||||
claimedAmount = 0;
|
||||
@@ -116,14 +134,13 @@ export async function claimHourlyForUser(userId: number): Promise<ClaimResult> {
|
||||
SET balance = $2,
|
||||
last_claim_at = $3
|
||||
WHERE user_id = $1
|
||||
RETURNING user_id, balance, last_claim_at
|
||||
RETURNING user_id, balance, last_claim_at, free_spins_bob_remaining, free_spins_bob_bet
|
||||
`,
|
||||
[userId, newBalance, newLastClaim]
|
||||
);
|
||||
|
||||
wallet = updated.rows[0];
|
||||
|
||||
// Transaktion für History
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO wallet_transactions (user_id, amount, reason)
|
||||
@@ -131,9 +148,6 @@ export async function claimHourlyForUser(userId: number): Promise<ClaimResult> {
|
||||
`,
|
||||
[userId, claimedAmount, "hourly_claim"]
|
||||
);
|
||||
|
||||
// nach einem erfolgreichen Claim ist der nächste in 1h
|
||||
nextClaimInMs = CLAIM_INTERVAL_MS;
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
|
||||
Reference in New Issue
Block a user