feature updates

This commit is contained in:
2026-02-02 16:28:53 -05:00
parent bf9aa3fa23
commit 44c06530ac
83 changed files with 282641 additions and 11251 deletions

View 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;
}

View 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")
}