diff --git a/src/__tests__/unit/team-generator.test.ts b/src/__tests__/unit/team-generator.test.ts new file mode 100644 index 0000000..ec7778a --- /dev/null +++ b/src/__tests__/unit/team-generator.test.ts @@ -0,0 +1,281 @@ +/** + * Unit Tests: Team Generation Algorithms + * + * Tests the correctness of team generation algorithms + * for different partner rotation strategies. + */ + +import { describe, test, expect } from 'bun:test'; +import { + generateTeams, + generateRandomTeams, + generateEvenTeams, + generateELOBasedTeams, + calculateTeamBalance, + calculatePartnershipFrequency, + generateTeamsWithRotation, + type Player, + type Team, + type PartnerRotation, +} from '@/lib/team-generator'; + +// Test players with varying ELO ratings +const testPlayers: Player[] = [ + { id: 1, name: 'Alice', currentElo: 1500 }, + { id: 2, name: 'Bob', currentElo: 1200 }, + { id: 3, name: 'Charlie', currentElo: 1400 }, + { id: 4, name: 'Diana', currentElo: 1300 }, + { id: 5, name: 'Eve', currentElo: 1100 }, + { id: 6, name: 'Frank', currentElo: 1600 }, +]; + +describe('Team Generation Algorithms', () => { + describe('generateTeams', () => { + test('should return empty teams for fewer than 2 players', () => { + const result = generateTeams([], 'none', true); + expect(result.teams).toEqual([]); + expect(result.byePlayer).toBeNull(); + }); + + test('should generate one team for 2 players', () => { + const players = testPlayers.slice(0, 2); + const result = generateTeams(players, 'none', true); + expect(result.teams).toHaveLength(1); + expect(result.teams[0].player1Id).toBeDefined(); + expect(result.teams[0].player2Id).toBeDefined(); + }); + + test('should generate correct number of teams for even player count', () => { + const players = testPlayers.slice(0, 6); + const result = generateTeams(players, 'none', true); + expect(result.teams).toHaveLength(3); + }); + + test('should handle odd player count with byes enabled', () => { + const players = testPlayers.slice(0, 5); + const result = generateTeams(players, 'none', true); + expect(result.teams).toHaveLength(2); + expect(result.byePlayer).not.toBeNull(); + // Bye goes to highest ELO player (player 1 with Elo 1500) + expect(result.byePlayer?.id).toBe(1); + }); + + test('should throw error for odd player count with byes disabled', () => { + const players = testPlayers.slice(0, 5); + expect(() => generateTeams(players, 'none', false)).toThrow( + "Odd number of participants. Enable 'Allow Byes' to proceed." + ); + }); + + test('should use different strategies correctly', () => { + const players = testPlayers.slice(0, 6); + + // Test each strategy + const strategies: PartnerRotation[] = ['none', 'minimize_repeat', 'maximize_even', 'elo_based']; + + for (const strategy of strategies) { + const result = generateTeams(players, strategy, true); + expect(result.teams).toHaveLength(3); + expect(result.strategy).toBe(strategy); + } + }); + }); + + describe('generateRandomTeams', () => { + test('should generate all teams with unique players', () => { + const players = testPlayers.slice(0, 6); + const teams = generateRandomTeams(players); + + expect(teams).toHaveLength(3); + + // Collect all player IDs + const allPlayerIds = teams.flatMap(t => [t.player1Id, t.player2Id]); + const uniqueIds = new Set(allPlayerIds); + + expect(uniqueIds.size).toBe(6); + }); + + test('should not create duplicate teams', () => { + const players = testPlayers.slice(0, 6); + const teams = generateRandomTeams(players); + + // Check that no team has the same pair + const teamKeys = teams.map(t => [t.player1Id, t.player2Id].sort().join('-')); + const uniqueKeys = new Set(teamKeys); + + expect(uniqueKeys.size).toBe(teams.length); + }); + + test('should include team names', () => { + const players = testPlayers.slice(0, 4); + const teams = generateRandomTeams(players); + + for (const team of teams) { + expect(team.teamName).toContain('&'); + } + }); + }); + + describe('generateEvenTeams', () => { + test('should pair top players with bottom players', () => { + const players = testPlayers.slice(0, 6); + const teams = generateEvenTeams(players); + + expect(teams).toHaveLength(3); + + // Check that teams are balanced + const playerMap = new Map(players.map(p => [p.id, p])); + let totalDiff = 0; + + for (const team of teams) { + const player1 = playerMap.get(team.player1Id)!; + const player2 = playerMap.get(team.player2Id)!; + totalDiff += Math.abs(player1.currentElo - player2.currentElo); + } + + // Average difference should be reasonable + const avgDiff = totalDiff / teams.length; + expect(avgDiff).toBeLessThan(500); // Should be reasonably balanced + }); + + test('should handle odd number of players', () => { + const players = testPlayers.slice(0, 5); + const teams = generateEvenTeams(players); + + expect(teams).toHaveLength(2); + }); + }); + + describe('generateELOBasedTeams', () => { + test('should pair strongest with weakest', () => { + const players = testPlayers.slice(0, 6); + const teams = generateELOBasedTeams(players); + + expect(teams).toHaveLength(3); + + // Check pairing pattern + const sorted = [...players].sort((a, b) => b.currentElo - a.currentElo); + + for (let i = 0; i < teams.length; i++) { + const team = teams[i]; + const expectedPlayer1 = sorted[i]; + const expectedPlayer2 = sorted[sorted.length - 1 - i]; + + expect(team.player1Id).toBe(expectedPlayer1.id); + expect(team.player2Id).toBe(expectedPlayer2.id); + } + }); + + test('should create balanced teams', () => { + const players = testPlayers.slice(0, 6); + const teams = generateELOBasedTeams(players); + + const playerMap = new Map(players.map(p => [p.id, p])); + let totalDiff = 0; + + for (const team of teams) { + const player1 = playerMap.get(team.player1Id)!; + const player2 = playerMap.get(team.player2Id)!; + totalDiff += Math.abs(player1.currentElo - player2.currentElo); + } + + // Average difference should be high for ELO-based pairing + const avgDiff = totalDiff / teams.length; + expect(avgDiff).toBeGreaterThan(200); + }); + }); + + describe('calculateTeamBalance', () => { + test('should calculate balance for well-balanced teams', () => { + const teams: Team[] = [ + { player1Id: 1, player2Id: 2, teamName: 'Test' }, + { player1Id: 3, player2Id: 4, teamName: 'Test' }, + ]; + + const balance = calculateTeamBalance(teams, testPlayers); + expect(balance).toBeGreaterThan(0); + }); + + test('should return 0 for empty teams', () => { + const balance = calculateTeamBalance([], testPlayers); + expect(balance).toBe(0); + }); + }); + + describe('calculatePartnershipFrequency', () => { + test('should count partnerships correctly', () => { + const team1: Team[] = [ + { player1Id: 1, player2Id: 2, teamName: 'Test' }, + { player1Id: 3, player2Id: 4, teamName: 'Test' }, + ]; + + const team2: Team[] = [ + { player1Id: 1, player2Id: 3, teamName: 'Test' }, + { player1Id: 2, player2Id: 4, teamName: 'Test' }, + ]; + + const frequency = calculatePartnershipFrequency([team1, team2], testPlayers); + + expect(frequency.get('1-2')).toBe(1); + expect(frequency.get('3-4')).toBe(1); + expect(frequency.get('1-3')).toBe(1); + expect(frequency.get('2-4')).toBe(1); + }); + + test('should handle empty previous teams', () => { + const frequency = calculatePartnershipFrequency([], testPlayers); + expect(frequency.size).toBe(0); + }); + }); + + describe('generateTeamsWithRotation', () => { + test('should minimize repeat partnerships', () => { + const players = testPlayers.slice(0, 6); + + // First round + const firstRound = generateTeams(players, 'none', true); + + // Second round with rotation + const secondRound = generateTeamsWithRotation( + players, + [firstRound.teams], + 'minimize_repeat', + true + ); + + // Check that partnerships are different + const firstRoundKeys = new Set( + firstRound.teams.map(t => [t.player1Id, t.player2Id].sort().join('-')) + ); + + for (const team of secondRound.teams) { + const key = [team.player1Id, team.player2Id].sort().join('-'); + expect(firstRoundKeys.has(key)).toBe(false); + } + }); + + test('should handle multiple previous rounds', () => { + const players = testPlayers.slice(0, 6); + + // Simulate 3 rounds + const previousTeams: Team[][] = []; + + for (let i = 0; i < 3; i++) { + const result = generateTeamsWithRotation( + players, + previousTeams, + 'minimize_repeat', + true + ); + previousTeams.push(result.teams); + } + + expect(previousTeams).toHaveLength(3); + + // Each round should have 3 teams + for (const teams of previousTeams) { + expect(teams).toHaveLength(3); + } + }); + }); +});