feature updates
This commit is contained in:
144
server/src/db/GameDatabase.ts
Normal file
144
server/src/db/GameDatabase.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
// GameDatabase - Database operations for game sessions and player stats
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { GameSessionData } from '../game/GameSession.js';
|
||||
import { EloResult } from '../game/EloCalculator.js';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// Default ELO rating for new players or players without stats
|
||||
const DEFAULT_ELO = 1000;
|
||||
|
||||
/**
|
||||
* Get a player's ELO rating from the database
|
||||
* Returns the player's ELO or default value if not found
|
||||
*/
|
||||
export async function getPlayerElo(userId: string): Promise<number> {
|
||||
try {
|
||||
const stats = await prisma.playerStats.findUnique({
|
||||
where: { userId },
|
||||
});
|
||||
return stats?.eloRating ?? DEFAULT_ELO;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching ELO for user ${userId}:`, error);
|
||||
return DEFAULT_ELO;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a completed match and update player stats
|
||||
*/
|
||||
export async function recordMatchResult(
|
||||
session: GameSessionData,
|
||||
eloResult: EloResult
|
||||
): Promise<void> {
|
||||
const player1 = session.players[0];
|
||||
const player2 = session.players[1];
|
||||
|
||||
try {
|
||||
// Use a transaction to ensure all updates happen together
|
||||
await prisma.$transaction(async (tx) => {
|
||||
// Create the match record
|
||||
await tx.match.create({
|
||||
data: {
|
||||
player1Id: player1.userId,
|
||||
player2Id: player2.userId,
|
||||
winnerId: session.winnerId,
|
||||
player1Deck: player1.deckId,
|
||||
player2Deck: player2.deckId,
|
||||
result: session.endReason,
|
||||
turns: session.turnNumber,
|
||||
eloChange: Math.abs(eloResult.player1Change), // Store absolute change
|
||||
},
|
||||
});
|
||||
|
||||
// Update player 1 stats (upsert in case stats don't exist yet)
|
||||
const player1Won = session.winnerId === player1.userId;
|
||||
await tx.playerStats.upsert({
|
||||
where: { userId: player1.userId },
|
||||
create: {
|
||||
userId: player1.userId,
|
||||
wins: player1Won ? 1 : 0,
|
||||
losses: player1Won ? 0 : 1,
|
||||
eloRating: eloResult.player1NewElo,
|
||||
gamesPlayed: 1,
|
||||
},
|
||||
update: {
|
||||
wins: { increment: player1Won ? 1 : 0 },
|
||||
losses: { increment: player1Won ? 0 : 1 },
|
||||
eloRating: eloResult.player1NewElo,
|
||||
gamesPlayed: { increment: 1 },
|
||||
},
|
||||
});
|
||||
|
||||
// Update player 2 stats (upsert in case stats don't exist yet)
|
||||
const player2Won = session.winnerId === player2.userId;
|
||||
await tx.playerStats.upsert({
|
||||
where: { userId: player2.userId },
|
||||
create: {
|
||||
userId: player2.userId,
|
||||
wins: player2Won ? 1 : 0,
|
||||
losses: player2Won ? 0 : 1,
|
||||
eloRating: eloResult.player2NewElo,
|
||||
gamesPlayed: 1,
|
||||
},
|
||||
update: {
|
||||
wins: { increment: player2Won ? 1 : 0 },
|
||||
losses: { increment: player2Won ? 0 : 1 },
|
||||
eloRating: eloResult.player2NewElo,
|
||||
gamesPlayed: { increment: 1 },
|
||||
},
|
||||
});
|
||||
|
||||
// Update last login timestamps for both players
|
||||
const now = new Date();
|
||||
await tx.user.update({
|
||||
where: { id: player1.userId },
|
||||
data: { lastLogin: now },
|
||||
});
|
||||
await tx.user.update({
|
||||
where: { id: player2.userId },
|
||||
data: { lastLogin: now },
|
||||
});
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Match recorded: ${player1.username} vs ${player2.username}, ` +
|
||||
`winner=${session.winnerId === player1.userId ? player1.username : player2.username}, ` +
|
||||
`reason=${session.endReason}`
|
||||
);
|
||||
console.log(
|
||||
`ELO updated: ${player1.username}: ${eloResult.player1NewElo} (${eloResult.player1Change > 0 ? '+' : ''}${eloResult.player1Change}), ` +
|
||||
`${player2.username}: ${eloResult.player2NewElo} (${eloResult.player2Change > 0 ? '+' : ''}${eloResult.player2Change})`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error recording match result:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get multiple player ELO ratings in a single query
|
||||
*/
|
||||
export async function getPlayersElo(userIds: string[]): Promise<Map<string, number>> {
|
||||
const result = new Map<string, number>();
|
||||
|
||||
try {
|
||||
const stats = await prisma.playerStats.findMany({
|
||||
where: { userId: { in: userIds } },
|
||||
select: { userId: true, eloRating: true },
|
||||
});
|
||||
|
||||
// Initialize all with default ELO
|
||||
userIds.forEach(id => result.set(id, DEFAULT_ELO));
|
||||
|
||||
// Override with actual values from database
|
||||
stats.forEach(s => result.set(s.userId, s.eloRating));
|
||||
} catch (error) {
|
||||
console.error('Error fetching player ELOs:', error);
|
||||
// Return defaults on error
|
||||
userIds.forEach(id => result.set(id, DEFAULT_ELO));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
92
server/src/db/schema.prisma
Normal file
92
server/src/db/schema.prisma
Normal file
@@ -0,0 +1,92 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(uuid())
|
||||
email String @unique
|
||||
emailVerified Boolean @default(false) @map("email_verified")
|
||||
passwordHash String @map("password_hash")
|
||||
username String @unique
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
lastLogin DateTime? @map("last_login")
|
||||
|
||||
stats PlayerStats?
|
||||
decks Deck[]
|
||||
matchesAsPlayer1 Match[] @relation("Player1Matches")
|
||||
matchesAsPlayer2 Match[] @relation("Player2Matches")
|
||||
matchesWon Match[] @relation("WinnerMatches")
|
||||
verificationTokens VerificationToken[]
|
||||
passwordResetTokens PasswordResetToken[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model PlayerStats {
|
||||
userId String @id @map("user_id")
|
||||
wins Int @default(0)
|
||||
losses Int @default(0)
|
||||
eloRating Int @default(1000) @map("elo_rating")
|
||||
gamesPlayed Int @default(0) @map("games_played")
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("player_stats")
|
||||
}
|
||||
|
||||
model Deck {
|
||||
id String @id @default(uuid())
|
||||
userId String @map("user_id")
|
||||
name String
|
||||
cardIds Json @map("card_ids")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("decks")
|
||||
}
|
||||
|
||||
model Match {
|
||||
id String @id @default(uuid())
|
||||
player1Id String @map("player1_id")
|
||||
player2Id String @map("player2_id")
|
||||
winnerId String? @map("winner_id")
|
||||
player1Deck Json? @map("player1_deck")
|
||||
player2Deck Json? @map("player2_deck")
|
||||
result String? // 'damage', 'deck_out', 'concede', 'timeout'
|
||||
turns Int?
|
||||
eloChange Int? @map("elo_change")
|
||||
playedAt DateTime @default(now()) @map("played_at")
|
||||
|
||||
player1 User @relation("Player1Matches", fields: [player1Id], references: [id])
|
||||
player2 User @relation("Player2Matches", fields: [player2Id], references: [id])
|
||||
winner User? @relation("WinnerMatches", fields: [winnerId], references: [id])
|
||||
|
||||
@@map("matches")
|
||||
}
|
||||
|
||||
model VerificationToken {
|
||||
token String @id
|
||||
userId String @map("user_id")
|
||||
expiresAt DateTime @map("expires_at")
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("verification_tokens")
|
||||
}
|
||||
|
||||
model PasswordResetToken {
|
||||
token String @id
|
||||
userId String @map("user_id")
|
||||
expiresAt DateTime @map("expires_at")
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("password_reset_tokens")
|
||||
}
|
||||
Reference in New Issue
Block a user