/** * Team Generation Algorithms * * Provides algorithms for generating teams in tournaments * based on different partner rotation strategies. */ export type PartnerRotation = 'none' | 'minimize_repeat' | 'maximize_even' | 'elo_based' export interface Player { id: number currentElo: number name: string } export interface Team { player1Id: number player2Id: number teamName: string | null } export interface TeamGenerationResult { teams: Team[] byePlayer: Player | null strategy: PartnerRotation } /** * Generate teams based on partner rotation strategy */ export function generateTeams( players: Player[], strategy: PartnerRotation, allowByes: boolean ): TeamGenerationResult { if (players.length < 2) { return { teams: [], byePlayer: null, strategy } } // Handle odd number of players let byePlayer: Player | null = null let workingPlayers = [...players] if (workingPlayers.length % 2 !== 0) { if (!allowByes) { throw new Error("Odd number of participants. Enable 'Allow Byes' to proceed.") } // Remove the player with the lowest ELO for bye workingPlayers.sort((a, b) => a.currentElo - b.currentElo) byePlayer = workingPlayers.pop() || null } let teams: Team[] switch (strategy) { case 'none': // Random pairing teams = generateRandomTeams(workingPlayers) break case 'minimize_repeat': // For initial generation, we can't minimize repeats since there are no previous teams // So we just generate random teams teams = generateRandomTeams(workingPlayers) break case 'maximize_even': // Pair players to maximize competitive balance teams = generateEvenTeams(workingPlayers) break case 'elo_based': // Pair strongest with weakest teams = generateELOBasedTeams(workingPlayers) break default: teams = generateRandomTeams(workingPlayers) } return { teams, byePlayer, strategy } } /** * Generate random teams using Fisher-Yates shuffle */ export function generateRandomTeams(players: Player[]): Team[] { const shuffled = [...players] // Fisher-Yates shuffle for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)) ;[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]] } return createTeamsFromPairs(shuffled) } /** * Generate teams to maximize competitive balance * Pairs top half with bottom half by ELO */ export function generateEvenTeams(players: Player[]): Team[] { // Sort by ELO descending const sorted = [...players].sort((a, b) => b.currentElo - a.currentElo) // Split into two halves const midpoint = Math.floor(sorted.length / 2) const topHalf = sorted.slice(0, midpoint) const bottomHalf = sorted.slice(midpoint) // Interleave: pair top players with bottom players const interleaved: Player[] = [] const maxLen = Math.max(topHalf.length, bottomHalf.length) for (let i = 0; i < maxLen; i++) { if (i < topHalf.length) interleaved.push(topHalf[i]) if (i < bottomHalf.length) interleaved.push(bottomHalf[i]) } return createTeamsFromPairs(interleaved) } /** * Generate ELO-based teams (strongest + weakest pairing) * Pairs highest with lowest, 2nd highest with 2nd lowest, etc. */ export function generateELOBasedTeams(players: Player[]): Team[] { // Sort by ELO descending const sorted = [...players].sort((a, b) => b.currentElo - a.currentElo) const teams: Team[] = [] // Pair strongest with weakest for (let i = 0; i < Math.floor(sorted.length / 2); i++) { const j = sorted.length - 1 - i if (i >= j) break teams.push({ player1Id: sorted[i].id, player2Id: sorted[j].id, teamName: `${sorted[i].name} & ${sorted[j].name}`, }) } return teams } /** * Helper function to create teams from pairs of players */ function createTeamsFromPairs(players: Player[]): Team[] { const teams: Team[] = [] for (let i = 0; i < players.length - 1; i += 2) { teams.push({ player1Id: players[i].id, player2Id: players[i + 1].id, teamName: `${players[i].name} & ${players[i + 1].name}`, }) } return teams } /** * Calculate ELO balance score for a set of teams * Higher score means more balanced teams */ export function calculateTeamBalance(teams: Team[], players: Player[]): number { const playerMap = new Map(players.map(p => [p.id, p])) let totalBalance = 0 let validTeams = 0 for (const team of teams) { const player1 = playerMap.get(team.player1Id) const player2 = playerMap.get(team.player2Id) if (player1 && player2) { // Balance is higher when ELOs are closer const diff = Math.abs(player1.currentElo - player2.currentElo) totalBalance += diff validTeams++ } } // Return average ELO difference (lower is better balanced) return validTeams > 0 ? totalBalance / validTeams : 0 } /** * Calculate partnership frequency for a set of teams * Returns a map of partnership pairs to their count */ export function calculatePartnershipFrequency( allTeams: Team[][], players: Player[] ): Map { const frequency = new Map() for (const roundTeams of allTeams) { for (const team of roundTeams) { // Create sorted key to handle both orderings const key = [team.player1Id, team.player2Id].sort().join('-') frequency.set(key, (frequency.get(key) || 0) + 1) } } return frequency } /** * Generate teams with partner rotation to minimize repeats * This algorithm tries to avoid pairing players who have already partnered together */ export function generateTeamsWithRotation( players: Player[], previousTeams: Team[][], strategy: PartnerRotation = 'none', allowByes: boolean = true ): TeamGenerationResult { if (players.length < 2) { return { teams: [], byePlayer: null, strategy } } // Calculate partnership frequency from previous rounds const partnershipFreq = calculatePartnershipFrequency(previousTeams, players) // Handle odd number of players let byePlayer: Player | null = null let workingPlayers = [...players] if (workingPlayers.length % 2 !== 0) { if (!allowByes) { throw new Error("Odd number of participants. Enable 'Allow Byes' to proceed.") } // Remove the player with the lowest ELO for bye workingPlayers.sort((a, b) => a.currentElo - b.currentElo) byePlayer = workingPlayers.pop() || null } // Generate teams based on strategy, avoiding repeat partnerships let teams: Team[] switch (strategy) { case 'minimize_repeat': teams = generateTeamsMinimizingRepeats(workingPlayers, partnershipFreq) break case 'maximize_even': teams = generateEvenTeamsAvoidingRepeats(workingPlayers, partnershipFreq) break case 'elo_based': teams = generateELOBasedTeamsAvoidingRepeats(workingPlayers, partnershipFreq) break default: teams = generateRandomTeams(workingPlayers) } return { teams, byePlayer, strategy } } /** * Generate teams minimizing repeat partnerships */ function generateTeamsMinimizingRepeats( players: Player[], partnershipFreq: Map ): Team[] { const teams: Team[] = [] const used = new Set() // Sort players by number of partnerships (least partnered first) const playersWithPartnershipCount = players.map(p => { let count = 0 for (const [key, freq] of partnershipFreq) { const [id1, id2] = key.split('-').map(Number) if (id1 === p.id || id2 === p.id) { count += freq } } return { player: p, partnerships: count } }) playersWithPartnershipCount.sort((a, b) => a.partnerships - b.partnerships) // Greedy algorithm: pair least-partnered players first for (let i = 0; i < playersWithPartnershipCount.length; i++) { if (used.has(playersWithPartnershipCount[i].player.id)) continue let bestPartner = -1 let bestScore = Infinity for (let j = i + 1; j < playersWithPartnershipCount.length; j++) { if (used.has(playersWithPartnershipCount[j].player.id)) continue const key = [ playersWithPartnershipCount[i].player.id, playersWithPartnershipCount[j].player.id ].sort().join('-') const freq = partnershipFreq.get(key) || 0 if (freq < bestScore) { bestScore = freq bestPartner = j } } if (bestPartner !== -1) { teams.push({ player1Id: playersWithPartnershipCount[i].player.id, player2Id: playersWithPartnershipCount[bestPartner].player.id, teamName: `${playersWithPartnershipCount[i].player.name} & ${playersWithPartnershipCount[bestPartner].player.name}`, }) used.add(playersWithPartnershipCount[i].player.id) used.add(playersWithPartnershipCount[bestPartner].player.id) } } return teams } /** * Generate even teams while avoiding repeat partnerships */ function generateEvenTeamsAvoidingRepeats( players: Player[], partnershipFreq: Map ): Team[] { // Start with even teams const baseTeams = generateEvenTeams(players) // Try to improve by swapping to reduce repeat partnerships return optimizeTeamsForRepeats(baseTeams, players, partnershipFreq) } /** * Generate ELO-based teams while avoiding repeat partnerships */ function generateELOBasedTeamsAvoidingRepeats( players: Player[], partnershipFreq: Map ): Team[] { // Start with ELO-based teams const baseTeams = generateELOBasedTeams(players) // Try to improve by swapping to reduce repeat partnerships return optimizeTeamsForRepeats(baseTeams, players, partnershipFreq) } /** * Optimize teams by swapping players to reduce repeat partnerships */ function optimizeTeamsForRepeats( teams: Team[], players: Player[], partnershipFreq: Map ): Team[] { if (teams.length < 2) return teams let improved = true let iterations = 0 const maxIterations = 100 while (improved && iterations < maxIterations) { improved = false iterations++ for (let i = 0; i < teams.length; i++) { for (let j = i + 1; j < teams.length; j++) { // Try swapping player1 of team i with player1 of team j const newTeams = [...teams] const temp = newTeams[i].player1Id newTeams[i] = { ...newTeams[i], player1Id: newTeams[j].player1Id } newTeams[j] = { ...newTeams[j], player1Id: temp } // Calculate current frequency const currentFreq = calculateTeamFrequency(teams[i], partnershipFreq) + calculateTeamFrequency(teams[j], partnershipFreq) // Calculate new frequency const newFreq = calculateTeamFrequency(newTeams[i], partnershipFreq) + calculateTeamFrequency(newTeams[j], partnershipFreq) if (newFreq < currentFreq) { teams = newTeams improved = true } } } } return teams } /** * Calculate partnership frequency for a single team */ function calculateTeamFrequency( team: Team, partnershipFreq: Map ): number { const key = [team.player1Id, team.player2Id].sort().join('-') return partnershipFreq.get(key) || 0 }