/** * Unit Tests: Elo Rating System * * Tests the mathematical correctness of Elo calculations and rating updates */ import { describe, test, expect } from 'vitest'; import { calculateEloChange, calculateExpectedScore, calculateTeamElo } from '@/lib/elo-utils'; describe('Elo Rating System', () => { const K_FACTOR = 32; describe('calculateExpectedScore', () => { test('should calculate expected score for equal ratings', () => { const ratingA = 1500; const ratingB = 1500; const expected = calculateExpectedScore(ratingA, ratingB); // Equal ratings should result in 0.5 expected score expect(expected).toBeCloseTo(0.5, 4); }); test('should calculate expected score when A is stronger', () => { const ratingA = 1600; const ratingB = 1500; const expected = calculateExpectedScore(ratingA, ratingB); // Higher rated player should have higher expected score expect(expected).toBeGreaterThan(0.5); expect(expected).toBeLessThan(1); }); test('should calculate expected score when B is stronger', () => { const ratingA = 1500; const ratingB = 1600; const expected = calculateExpectedScore(ratingA, ratingB); // Lower rated player should have lower expected score expect(expected).toBeLessThan(0.5); expect(expected).toBeGreaterThan(0); }); test('should handle large rating differences', () => { const ratingA = 2000; const ratingB = 1000; const expected = calculateExpectedScore(ratingA, ratingB); // Much higher rated player should have very high expected score expect(expected).toBeGreaterThan(0.95); }); }); describe('calculateEloChange', () => { test('should return positive change for win against equal opponent', () => { const rating = 1500; const opponentRating = 1500; const actualScore = 1; // Win const change = calculateEloChange(rating, opponentRating, actualScore, K_FACTOR); expect(change).toBeGreaterThan(0); // For equal opponents, expected = 0.5, so change = K_FACTOR * (1 - 0.5) = K_FACTOR / 2 expect(change).toBeCloseTo(K_FACTOR / 2, 0); }); test('should return negative change for loss against equal opponent', () => { const rating = 1500; const opponentRating = 1500; const actualScore = 0; // Loss const change = calculateEloChange(rating, opponentRating, actualScore, K_FACTOR); expect(change).toBeLessThan(0); // For equal opponents, expected = 0.5, so change = K_FACTOR * (0 - 0.5) = -K_FACTOR / 2 expect(change).toBeCloseTo(-K_FACTOR / 2, 0); }); test('should return zero change for draw against equal opponent', () => { const rating = 1500; const opponentRating = 1500; const actualScore = 0.5; // Draw const change = calculateEloChange(rating, opponentRating, actualScore, K_FACTOR); expect(change).toBeCloseTo(0, 1); }); test('should return larger positive change for win against stronger opponent', () => { const rating = 1500; const opponentRating = 1800; // Stronger const actualScore = 1; // Win (upset) const change = calculateEloChange(rating, opponentRating, actualScore, K_FACTOR); // Should gain more than K_FACTOR / 2 for upset win (since expected score is very low) expect(change).toBeGreaterThan(K_FACTOR / 2); }); test('should return larger negative change for loss against weaker opponent', () => { const rating = 1800; const opponentRating = 1500; // Weaker const actualScore = 0; // Loss (upset) const change = calculateEloChange(rating, opponentRating, actualScore, K_FACTOR); // Should lose more than K_FACTOR / 2 for upset loss (since expected score is very high) expect(change).toBeLessThan(-K_FACTOR / 2); }); }); describe('calculateTeamElo', () => { test('should average team member ratings', () => { const player1Rating = 1600; const player2Rating = 1400; const teamRating = calculateTeamElo(player1Rating, player2Rating); expect(teamRating).toBe(1500); }); test('should handle different team compositions', () => { const team1 = calculateTeamElo(1500, 1500); const team2 = calculateTeamElo(1400, 1600); const team3 = calculateTeamElo(1200, 1800); // All should average to 1500 expect(team1).toBe(1500); expect(team2).toBe(1500); expect(team3).toBe(1500); }); }); describe('Elo Change Distribution', () => { test('should conserve total rating points in system', () => { const player1Rating = 1500; const player2Rating = 1500; const player3Rating = 1500; const player4Rating = 1500; // Simulate a match where team 1 wins const team1Rating = calculateTeamElo(player1Rating, player2Rating); const team2Rating = calculateTeamElo(player3Rating, player4Rating); // Team 1 wins (score = 1, team 2 score = 0) const team1Change = calculateEloChange(team1Rating, team2Rating, 1, K_FACTOR); const team2Change = calculateEloChange(team2Rating, team1Rating, 0, K_FACTOR); // Split changes between team members const player1Change = team1Change / 2; const player2Change = team1Change / 2; const player3Change = team2Change / 2; const player4Change = team2Change / 2; // Total change should be zero (conservation of points) const totalChange = player1Change + player2Change + player3Change + player4Change; expect(totalChange).toBeCloseTo(0, 10); }); test('should calculate correct ratings after match', () => { const p1Initial = 1500; const p2Initial = 1500; const p3Initial = 1500; const p4Initial = 1500; // Team 1 wins const team1Rating = calculateTeamElo(p1Initial, p2Initial); const team2Rating = calculateTeamElo(p3Initial, p4Initial); const team1Change = calculateEloChange(team1Rating, team2Rating, 1, K_FACTOR); const team2Change = calculateEloChange(team2Rating, team1Rating, 0, K_FACTOR); const p1Change = team1Change / 2; const p2Change = team1Change / 2; const p3Change = team2Change / 2; const p4Change = team2Change / 2; // After match const p1Final = p1Initial + p1Change; const p2Final = p2Initial + p2Change; const p3Final = p3Initial + p3Change; const p4Final = p4Initial + p4Change; // Winners should have higher ratings expect(p1Final).toBeGreaterThan(p1Initial); expect(p2Final).toBeGreaterThan(p2Initial); // Losers should have lower ratings expect(p3Final).toBeLessThan(p3Initial); expect(p4Final).toBeLessThan(p4Initial); // All final ratings should still be close to 1500 expect(p1Final).toBeGreaterThan(1480); expect(p1Final).toBeLessThan(1520); }); }); describe('Rating Improvement Over Time', () => { test('should reflect consistent winning with rating increase', () => { let rating = 1500; const opponentRating = 1500; // Simulate 10 consecutive wins for (let i = 0; i < 10; i++) { rating += calculateEloChange(rating, opponentRating, 1, K_FACTOR); } // Rating should have increased significantly expect(rating).toBeGreaterThan(1600); }); test('should reflect consistent losing with rating decrease', () => { let rating = 1500; const opponentRating = 1500; // Simulate 10 consecutive losses for (let i = 0; i < 10; i++) { rating += calculateEloChange(rating, opponentRating, 0, K_FACTOR); } // Rating should have decreased significantly expect(rating).toBeLessThan(1400); }); }); });