import type { PrismaClient } from '@prisma/client'; // Import Glicko2 using require to avoid TypeScript issues with the module declaration const Glicko2 = require('glicko2').Glicko2; /** * Glicko2 Rating Utilities * * Provides functions for calculating Glicko2 ratings * Glicko2 is a rating system that accounts for rating reliability (deviation) */ // Glicko2 settings const settings = { tau: 0.5, // constrains volatility rating: 1500, rd: 350, vol: 0.06 }; /** * Create a new Glicko2 instance * @returns Glicko2 instance */ export function createGlicko2Instance() { return new Glicko2(settings); } /** * Create a new Glicko2 player * @returns Glicko2 player object */ export function createGlicko2Player() { const glicko = createGlicko2Instance(); return glicko.makePlayer(); } /** * Convert Prisma Glicko2 rating to Glicko2 player * @param ratingRecord Prisma Glicko2Rating record * @returns Glicko2 player object */ export function toGlicko2Player(ratingRecord: { rating: number; deviation: number; volatility: number; }) { const glicko = new Glicko2(settings); return glicko.makePlayer( ratingRecord.rating, ratingRecord.deviation, ratingRecord.volatility ); } /** * Convert Glicko2 player to Prisma Glicko2 rating format * @param player Glicko2 player object * @returns Object with rating, deviation, and volatility */ export function fromGlicko2Player(player: any): { rating: number; deviation: number; volatility: number; } { return { rating: player.getRating(), deviation: player.getRd(), volatility: player.getVol() }; } /** * Calculate Glicko2 ratings for a match * @param team1Players Array of Glicko2 players for team 1 * @param team2Players Array of Glicko2 players for team 2 * @param team1Won Whether team 1 won * @param team2Won Whether team 2 won * @param isTie Whether the match was a tie * @returns Updated player ratings */ export function calculateGlicko2Ratings( team1Players: any[], team2Players: any[], team1Won: boolean, team2Won: boolean, isTie: boolean ): { player: any; newRating: number; newDeviation: number; newVolatility: number }[] { const glicko = new Glicko2(settings); // Convert players to Glicko2 format if needed const processedTeam1 = team1Players.map(p => typeof p.getRating === 'function' ? p : glicko.makePlayer(p.rating, p.deviation, p.volatility) ); const processedTeam2 = team2Players.map(p => typeof p.getRating === 'function' ? p : glicko.makePlayer(p.rating, p.deviation, p.volatility) ); // Create matches in Glicko2 format // Glicko2 expects matches as [player1, player2, outcome] // where outcome is 1 for player1 win, 0.5 for draw, 0 for player2 win const matches: [any, any, number][] = []; // For team matches, we create individual matchups between players // This is a simplified approach - in practice, you might want a more sophisticated model for (const p1 of processedTeam1) { for (const p2 of processedTeam2) { let outcome: number; if (isTie) { outcome = 0.5; // Draw } else if (team1Won) { outcome = 1; // Team 1 player wins } else { outcome = 0; // Team 2 player wins } matches.push([p1, p2, outcome]); } } // Update ratings glicko.updateRatings(matches); // Return updated players return [ ...processedTeam1.map(p => ({ player: p, newRating: p.getRating(), newDeviation: p.getRd(), newVolatility: p.getVol() })), ...processedTeam2.map(p => ({ player: p, newRating: p.getRating(), newDeviation: p.getRd(), newVolatility: p.getVol() })) ]; } /** * Update player Glicko2 statistics after a match * @param prisma Prisma client instance * @param matchData Match data with player IDs and results * @returns Promise */ export async function updateGlicko2Ratings( prisma: PrismaClient, matchData: { team1Players: number[]; team2Players: number[]; team1Won: boolean; team2Won: boolean; isTie: boolean; } ): Promise { const { team1Players, team2Players, team1Won, team2Won, isTie } = matchData; // Get current ratings for all players const playerRecords = new Map(); for (const playerId of [...team1Players, ...team2Players]) { try { const ratingRecord = await (prisma as any).glicko2Rating.findUnique({ where: { playerId }, }); if (ratingRecord) { playerRecords.set(playerId, { rating: ratingRecord.rating, deviation: ratingRecord.deviation, volatility: ratingRecord.volatility }); } else { playerRecords.set(playerId, { rating: 1500, deviation: 350, volatility: 0.06 }); } } catch (error) { // If the model doesn't exist yet, use default playerRecords.set(playerId, { rating: 1500, deviation: 350, volatility: 0.06 }); } } // Convert to Glicko2 players const glicko = new Glicko2(settings); const team1GlickoPlayers = team1Players.map(id => { const record = playerRecords.get(id)!; return glicko.makePlayer(record.rating, record.deviation, record.volatility); }); const team2GlickoPlayers = team2Players.map(id => { const record = playerRecords.get(id)!; return glicko.makePlayer(record.rating, record.deviation, record.volatility); }); // Calculate new ratings const updatedPlayers = calculateGlicko2Ratings( team1GlickoPlayers, team2GlickoPlayers, team1Won, team2Won, isTie ); // Update database // The updatedPlayers array contains both teams, so we need to match them back to player IDs let playerIndex = 0; for (const playerId of [...team1Players, ...team2Players]) { const updated = updatedPlayers[playerIndex]; await (prisma as any).glicko2Rating.upsert({ where: { playerId }, update: { rating: updated.newRating, deviation: updated.newDeviation, volatility: updated.newVolatility, gamesPlayed: { increment: 1 }, wins: team1Players.includes(playerId) && team1Won ? { increment: 1 } : team2Players.includes(playerId) && team2Won ? { increment: 1 } : undefined, losses: (team1Players.includes(playerId) && team2Won && !isTie) || (team2Players.includes(playerId) && team1Won && !isTie) ? { increment: 1 } : undefined, draws: isTie ? { increment: 1 } : undefined, lastUpdated: new Date(), }, create: { playerId, rating: updated.newRating, deviation: updated.newDeviation, volatility: updated.newVolatility, gamesPlayed: 1, wins: team1Players.includes(playerId) && team1Won ? 1 : team2Players.includes(playerId) && team2Won ? 1 : 0, losses: (team1Players.includes(playerId) && team2Won && !isTie) || (team2Players.includes(playerId) && team1Won && !isTie) ? 1 : 0, draws: isTie ? 1 : 0, lastUpdated: new Date(), }, }); playerIndex++; } } /** * Recalculate all Glicko2 ratings from match history * @param prisma Prisma client instance * @returns Object with counts of affected records */ export async function recalculateAllGlicko2(prisma: PrismaClient) { // Get all matches in chronological order const matches = await prisma.match.findMany({ orderBy: { playedAt: 'asc' }, include: { player1P1: true, player1P2: true, player2P1: true, player2P2: true, }, }); // Reset all Glicko2 ratings await (prisma as any).glicko2Rating.updateMany({ data: { rating: 1500, deviation: 350, volatility: 0.06, gamesPlayed: 0, wins: 0, losses: 0, draws: 0, }, }); // Initialize in-memory tracking const playerRatings = new Map(); // Initialize Glicko2 system const glicko = new Glicko2(settings); // Process each match for (const match of matches) { const { player1P1, player1P2, player2P1, player2P2, team1Score, team2Score } = match; // Skip matches with missing players if (!player1P1 || !player1P2 || !player2P1 || !player2P2) { continue; } // Get current ratings const getOrCreatePlayer = (playerId: number) => { if (!playerRatings.has(playerId)) { playerRatings.set(playerId, { rating: 1500, deviation: 350, volatility: 0.06 }); } const record = playerRatings.get(playerId)!; return glicko.makePlayer(record.rating, record.deviation, record.volatility); }; const p1 = getOrCreatePlayer(player1P1.id); const p2 = getOrCreatePlayer(player1P2.id); const p3 = getOrCreatePlayer(player2P1.id); const p4 = getOrCreatePlayer(player2P2.id); const team1Won = team1Score > team2Score; const team2Won = team2Score > team1Score; const isTie = team1Score === team2Score; // Create matches const matchesToUpdate: [any, any, number][] = []; const outcome = isTie ? 0.5 : team1Won ? 1 : 0; // Create matchups between all players [p1, p2].forEach(p1Player => { [p3, p4].forEach(p2Player => { matchesToUpdate.push([p1Player, p2Player, outcome]); }); }); // Update ratings glicko.updateRatings(matchesToUpdate); // Update in-memory ratings playerRatings.set(player1P1.id, { rating: p1.getRating(), deviation: p1.getRd(), volatility: p1.getVol() }); playerRatings.set(player1P2.id, { rating: p2.getRating(), deviation: p2.getRd(), volatility: p2.getVol() }); playerRatings.set(player2P1.id, { rating: p3.getRating(), deviation: p3.getRd(), volatility: p3.getVol() }); playerRatings.set(player2P2.id, { rating: p4.getRating(), deviation: p4.getRd(), volatility: p4.getVol() }); // Update database records await (prisma as any).glicko2Rating.upsert({ where: { playerId: player1P1.id }, update: { rating: p1.getRating(), deviation: p1.getRd(), volatility: p1.getVol(), gamesPlayed: { increment: 1 }, wins: team1Won ? { increment: 1 } : undefined, losses: !team1Won && !isTie ? { increment: 1 } : undefined, draws: isTie ? { increment: 1 } : undefined, }, create: { playerId: player1P1.id, rating: p1.getRating(), deviation: p1.getRd(), volatility: p1.getVol(), gamesPlayed: 1, wins: team1Won ? 1 : 0, losses: !team1Won && !isTie ? 1 : 0, draws: isTie ? 1 : 0, }, }); await (prisma as any).glicko2Rating.upsert({ where: { playerId: player1P2.id }, update: { rating: p2.getRating(), deviation: p2.getRd(), volatility: p2.getVol(), gamesPlayed: { increment: 1 }, wins: team1Won ? { increment: 1 } : undefined, losses: !team1Won && !isTie ? { increment: 1 } : undefined, draws: isTie ? { increment: 1 } : undefined, }, create: { playerId: player1P2.id, rating: p2.getRating(), deviation: p2.getRd(), volatility: p2.getVol(), gamesPlayed: 1, wins: team1Won ? 1 : 0, losses: !team1Won && !isTie ? 1 : 0, draws: isTie ? 1 : 0, }, }); await (prisma as any).glicko2Rating.upsert({ where: { playerId: player2P1.id }, update: { rating: p3.getRating(), deviation: p3.getRd(), volatility: p3.getVol(), gamesPlayed: { increment: 1 }, wins: team2Won ? { increment: 1 } : undefined, losses: !team2Won && !isTie ? { increment: 1 } : undefined, draws: isTie ? { increment: 1 } : undefined, }, create: { playerId: player2P1.id, rating: p3.getRating(), deviation: p3.getRd(), volatility: p3.getVol(), gamesPlayed: 1, wins: team2Won ? 1 : 0, losses: !team2Won && !isTie ? 1 : 0, draws: isTie ? 1 : 0, }, }); await (prisma as any).glicko2Rating.upsert({ where: { playerId: player2P2.id }, update: { rating: p4.getRating(), deviation: p4.getRd(), volatility: p4.getVol(), gamesPlayed: { increment: 1 }, wins: team2Won ? { increment: 1 } : undefined, losses: !team2Won && !isTie ? { increment: 1 } : undefined, draws: isTie ? { increment: 1 } : undefined, }, create: { playerId: player2P2.id, rating: p4.getRating(), deviation: p4.getRd(), volatility: p4.getVol(), gamesPlayed: 1, wins: team2Won ? 1 : 0, losses: !team2Won && !isTie ? 1 : 0, draws: isTie ? 1 : 0, }, }); } return { matchesProcessed: matches.length, playersUpdated: playerRatings.size, }; }