576dfda619
- Fixed unused variables in unit tests (elo.test.ts, permissions.test.ts) - Fixed unused variables in E2E tests (account-acceptance.test.ts, elo-ratings.test.ts, epic1-auth-registration.test.ts, global.setup.ts) - Fixed 'any' type usage in EditTournamentForm.test.tsx and auth-simple.test.ts - Fixed unescaped quotes and apostrophes in React components - Fixed TypeScript type errors in Navigation.tsx, test-api/page.tsx, and matches/upload/page.tsx - Fixed missing useEffect dependency in tournaments/[id]/entry/page.tsx - Updated eslint config to exclude test-api directory - All tests now pass linting and build successfully
225 lines
7.8 KiB
TypeScript
225 lines
7.8 KiB
TypeScript
/**
|
|
* 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);
|
|
});
|
|
});
|
|
});
|