make mines use redis
This commit is contained in:
parent
ad1739f7f4
commit
af18d7f423
4 changed files with 56 additions and 52 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
import { user } from '$lib/server/db/schema';
|
import { user } from '$lib/server/db/schema';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { redis } from '$lib/server/redis';
|
||||||
|
|
||||||
interface MinesSession {
|
interface MinesSession {
|
||||||
sessionToken: string;
|
sessionToken: string;
|
||||||
|
|
@ -16,16 +16,20 @@ interface MinesSession {
|
||||||
userId: number;
|
userId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MINES_SESSION_PREFIX = 'mines:session:';
|
||||||
export const activeGames = new Map<string, MinesSession>();
|
export const getSessionKey = (token: string) => `${MINES_SESSION_PREFIX}${token}`;
|
||||||
|
|
||||||
// Clean up old games every minute. (5 Minute system)
|
// 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()) {
|
const keys = await redis.keys(`${MINES_SESSION_PREFIX}*`);
|
||||||
|
for (const key of keys) {
|
||||||
|
const sessionRaw = await redis.get(key);
|
||||||
|
if (!sessionRaw) continue;
|
||||||
|
const game = JSON.parse(sessionRaw) as MinesSession;
|
||||||
if (now - game.lastActivity > 5 * 60 * 1000) {
|
if (now - game.lastActivity > 5 * 60 * 1000) {
|
||||||
if (game.revealedTiles.length === 0) {
|
if (game.revealedTiles.length === 0) {
|
||||||
try {
|
try {
|
||||||
const [userData] = await db
|
const [userData] = await db
|
||||||
.select({ baseCurrencyBalance: user.baseCurrencyBalance })
|
.select({ baseCurrencyBalance: user.baseCurrencyBalance })
|
||||||
.from(user)
|
.from(user)
|
||||||
|
|
@ -43,19 +47,22 @@ setInterval(async () => {
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
})
|
})
|
||||||
.where(eq(user.id, game.userId));
|
.where(eq(user.id, game.userId));
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to refund inactive game ${token}:`, error);
|
console.error(`Failed to refund inactive game ${game.sessionToken}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
activeGames.delete(token);
|
await redis.del(getSessionKey(game.sessionToken));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 60000);
|
}, 60000);
|
||||||
|
|
||||||
setInterval(async () => {
|
setInterval(async () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
for (const [token, game] of activeGames.entries()) {
|
const keys = await redis.keys(`${MINES_SESSION_PREFIX}*`);
|
||||||
|
for (const key of keys) {
|
||||||
|
const sessionRaw = await redis.get(key);
|
||||||
|
if (!sessionRaw) continue;
|
||||||
|
const game = JSON.parse(sessionRaw) as MinesSession;
|
||||||
if (game.status === 'active' && game.revealedTiles.length > 0 && now - game.lastActivity > 20000) {
|
if (game.status === 'active' && game.revealedTiles.length > 0 && now - game.lastActivity > 20000) {
|
||||||
try {
|
try {
|
||||||
const [userData] = await db
|
const [userData] = await db
|
||||||
|
|
@ -78,9 +85,9 @@ setInterval(async () => {
|
||||||
})
|
})
|
||||||
.where(eq(user.id, game.userId));
|
.where(eq(user.id, game.userId));
|
||||||
|
|
||||||
activeGames.delete(token);
|
await redis.del(getSessionKey(game.sessionToken));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to auto cashout game ${token}:`, error);
|
console.error(`Failed to auto cashout game ${game.sessionToken}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ import { error, json } from '@sveltejs/kit';
|
||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
import { user } from '$lib/server/db/schema';
|
import { user } from '$lib/server/db/schema';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { activeGames } from '$lib/server/games/mines';
|
import { redis } from '$lib/server/redis';
|
||||||
|
import { getSessionKey } from '$lib/server/games/mines';
|
||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
|
|
||||||
export const POST: RequestHandler = async ({ request }) => {
|
export const POST: RequestHandler = async ({ request }) => {
|
||||||
|
|
@ -17,7 +18,8 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { sessionToken } = await request.json();
|
const { sessionToken } = await request.json();
|
||||||
const game = activeGames.get(sessionToken);
|
const sessionRaw = await redis.get(getSessionKey(sessionToken));
|
||||||
|
const game = sessionRaw ? JSON.parse(sessionRaw) : null;
|
||||||
const userId = Number(session.user.id);
|
const userId = Number(session.user.id);
|
||||||
|
|
||||||
if (!game) {
|
if (!game) {
|
||||||
|
|
@ -36,18 +38,16 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
let payout: number;
|
let payout: number;
|
||||||
let newBalance: 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 no tiles revealed, treat as abort and return full bet.
|
||||||
if (game.revealedTiles.length === 0) {
|
if (game.revealedTiles.length === 0) {
|
||||||
payout = game.betAmount;
|
payout = game.betAmount;
|
||||||
newBalance = Math.round((currentBalance + payout) * 100000000) / 100000000;
|
newBalance = Math.round((currentBalance + payout) * 100000000) / 100000000;
|
||||||
} else {
|
} else {
|
||||||
// Calculate payout
|
|
||||||
payout = game.betAmount * game.currentMultiplier;
|
payout = game.betAmount * game.currentMultiplier;
|
||||||
const roundedPayout = Math.round(payout * 100000000) / 100000000;
|
const roundedPayout = Math.round(payout * 100000000) / 100000000;
|
||||||
newBalance = Math.round((currentBalance + roundedPayout) * 100000000) / 100000000;
|
newBalance = Math.round((currentBalance + roundedPayout) * 100000000) / 100000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
await tx
|
await tx
|
||||||
.update(user)
|
.update(user)
|
||||||
.set({
|
.set({
|
||||||
|
|
@ -56,8 +56,7 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
})
|
})
|
||||||
.where(eq(user.id, userId));
|
.where(eq(user.id, userId));
|
||||||
|
|
||||||
|
await redis.del(getSessionKey(sessionToken));
|
||||||
activeGames.delete(sessionToken);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
newBalance,
|
newBalance,
|
||||||
|
|
@ -74,4 +73,4 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
const errorMessage = e instanceof Error ? e.message : 'Internal server error';
|
const errorMessage = e instanceof Error ? e.message : 'Internal server error';
|
||||||
return json({ error: errorMessage }, { status: 400 });
|
return json({ error: errorMessage }, { status: 400 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import { auth } from '$lib/auth';
|
import { auth } from '$lib/auth';
|
||||||
import { error, json } from '@sveltejs/kit';
|
import { error, json } from '@sveltejs/kit';
|
||||||
import { activeGames, calculateMultiplier } from '$lib/server/games/mines';
|
import { calculateMultiplier } from '$lib/server/games/mines';
|
||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
import { user } from '$lib/server/db/schema';
|
import { user } from '$lib/server/db/schema';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { redis, } from '$lib/server/redis';
|
||||||
|
import { getSessionKey } from '$lib/server/games/mines';
|
||||||
|
|
||||||
export const POST: RequestHandler = async ({ request }) => {
|
export const POST: RequestHandler = async ({ request }) => {
|
||||||
const session = await auth.api.getSession({
|
const session = await auth.api.getSession({
|
||||||
|
|
@ -17,7 +19,8 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { sessionToken, tileIndex } = await request.json();
|
const { sessionToken, tileIndex } = await request.json();
|
||||||
const game = activeGames.get(sessionToken);
|
const sessionRaw = await redis.get(getSessionKey(sessionToken));
|
||||||
|
const game = sessionRaw ? JSON.parse(sessionRaw) : null;
|
||||||
|
|
||||||
if (!game) {
|
if (!game) {
|
||||||
return json({ error: 'Invalid session' }, { status: 400 });
|
return json({ error: 'Invalid session' }, { status: 400 });
|
||||||
|
|
@ -29,7 +32,6 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
|
|
||||||
game.lastActivity = Date.now();
|
game.lastActivity = Date.now();
|
||||||
|
|
||||||
|
|
||||||
if (game.minePositions.includes(tileIndex)) {
|
if (game.minePositions.includes(tileIndex)) {
|
||||||
game.status = 'lost';
|
game.status = 'lost';
|
||||||
const minePositions = game.minePositions;
|
const minePositions = game.minePositions;
|
||||||
|
|
@ -44,7 +46,6 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
|
|
||||||
const currentBalance = Number(userData.baseCurrencyBalance);
|
const currentBalance = Number(userData.baseCurrencyBalance);
|
||||||
|
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(user)
|
.update(user)
|
||||||
.set({
|
.set({
|
||||||
|
|
@ -53,9 +54,8 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
})
|
})
|
||||||
.where(eq(user.id, userId));
|
.where(eq(user.id, userId));
|
||||||
|
|
||||||
activeGames.delete(sessionToken);
|
await redis.del(getSessionKey(sessionToken));
|
||||||
|
|
||||||
|
|
||||||
return json({
|
return json({
|
||||||
hitMine: true,
|
hitMine: true,
|
||||||
minePositions,
|
minePositions,
|
||||||
|
|
@ -65,15 +65,14 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
game.mineCount,
|
game.mineCount,
|
||||||
game.betAmount
|
game.betAmount
|
||||||
);
|
);
|
||||||
|
|
||||||
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 userId = Number(session.user.id);
|
||||||
|
|
@ -97,7 +96,7 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
})
|
})
|
||||||
.where(eq(user.id, userId));
|
.where(eq(user.id, userId));
|
||||||
|
|
||||||
activeGames.delete(sessionToken);
|
await redis.del(getSessionKey(sessionToken));
|
||||||
|
|
||||||
return json({
|
return json({
|
||||||
hitMine: false,
|
hitMine: false,
|
||||||
|
|
@ -108,6 +107,8 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await redis.set(getSessionKey(sessionToken), JSON.stringify(game));
|
||||||
|
|
||||||
return json({
|
return json({
|
||||||
hitMine: false,
|
hitMine: false,
|
||||||
currentMultiplier: game.currentMultiplier,
|
currentMultiplier: game.currentMultiplier,
|
||||||
|
|
@ -118,4 +119,4 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
const errorMessage = e instanceof Error ? e.message : 'Internal server error';
|
const errorMessage = e instanceof Error ? e.message : 'Internal server error';
|
||||||
return json({ error: errorMessage }, { status: 400 });
|
return json({ error: errorMessage }, { status: 400 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -3,7 +3,8 @@ import { error, json } from '@sveltejs/kit';
|
||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
import { user } from '$lib/server/db/schema';
|
import { user } from '$lib/server/db/schema';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { activeGames } from '$lib/server/games/mines';
|
import { redis } from '$lib/server/redis';
|
||||||
|
import { getSessionKey } from '$lib/server/games/mines';
|
||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
|
|
||||||
export const POST: RequestHandler = async ({ request }) => {
|
export const POST: RequestHandler = async ({ request }) => {
|
||||||
|
|
@ -43,17 +44,12 @@ 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) {
|
||||||
positions.add(Math.floor(Math.random() * 25));
|
positions.add(Math.floor(Math.random() * 25));
|
||||||
}
|
}
|
||||||
const safePositions = [];
|
|
||||||
for (let i = 0; i < 25; i++) {
|
|
||||||
if (!positions.has(i)) safePositions.push(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
// transaction token for authentication stuff
|
// transaction token for authentication stuff
|
||||||
const randomBytes = new Uint8Array(8);
|
const randomBytes = new Uint8Array(8);
|
||||||
crypto.getRandomValues(randomBytes);
|
crypto.getRandomValues(randomBytes);
|
||||||
|
|
@ -64,19 +60,21 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const newBalance = roundedBalance - roundedAmount;
|
const newBalance = roundedBalance - roundedAmount;
|
||||||
|
|
||||||
// Create session
|
await redis.set(
|
||||||
activeGames.set(sessionToken, {
|
getSessionKey(sessionToken),
|
||||||
sessionToken,
|
JSON.stringify({
|
||||||
betAmount: roundedAmount,
|
sessionToken,
|
||||||
mineCount,
|
betAmount: roundedAmount,
|
||||||
minePositions: Array.from(positions),
|
mineCount,
|
||||||
revealedTiles: [],
|
minePositions: Array.from(positions),
|
||||||
startTime: now,
|
revealedTiles: [],
|
||||||
lastActivity: now,
|
startTime: now,
|
||||||
currentMultiplier: 1,
|
lastActivity: now,
|
||||||
status: 'active',
|
currentMultiplier: 1,
|
||||||
userId
|
status: 'active',
|
||||||
});
|
userId
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Update user balance
|
// Update user balance
|
||||||
await tx
|
await tx
|
||||||
|
|
@ -87,7 +85,6 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
})
|
})
|
||||||
.where(eq(user.id, userId));
|
.where(eq(user.id, userId));
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sessionToken,
|
sessionToken,
|
||||||
newBalance
|
newBalance
|
||||||
|
|
@ -100,4 +97,4 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
const errorMessage = e instanceof Error ? e.message : 'Internal server error';
|
const errorMessage = e instanceof Error ? e.message : 'Internal server error';
|
||||||
return json({ error: errorMessage }, { status: 400 });
|
return json({ error: errorMessage }, { status: 400 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Reference in a new issue