Files
euchre_camp/src/__tests__/unit/elo.test.ts
T
david d209803e30
Test / unit-tests (push) Has been cancelled
Pull Request / unit-tests (pull_request) Failing after 1m55s
Pull Request / acceptance-tests (pull_request) Has been skipped
Pull Request / analyze-bump-type (pull_request) Has been skipped
feat: migrate from npm to Bun
- Install Bun via mise
- Update package.json scripts to use Bun
- Migrate unit tests from Vitest to Bun test runner
- Migrate component tests from Vitest to Bun test runner
- Configure Bun with DOM environment for React Testing Library
- Keep Playwright for E2E tests (as planned)
- 93/100 tests passing (7 flaky tests when running all together, pass individually)
2026-04-01 00:12:05 -07:00

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 'bun:test';
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);
});
});
});