export interface MatchupPairing { player1P1Id: number player1P2Id: number player2P1Id: number player2P2Id: number } export interface RoundSchedule { roundNumber: number matchups: MatchupPairing[] } export interface TeamPairing { player1Id: number player2Id: number } /** * Generate a round-robin schedule using the circle method. * * For N teams, produces N-1 rounds where each team plays every other * team exactly once. If N is odd, a "bye" is added internally so one * team sits out each round (the bye matchup is excluded from output). * * @param teamPairings - Array of player pairings (each with 2 player IDs) * @returns Array of rounds, each containing matchup pairings */ export function generateRoundRobin( teamPairings: TeamPairing[] ): RoundSchedule[] { if (teamPairings.length < 2) { return [] } // Use circle method: fix first team, rotate the rest // If odd number of teams, add a sentinel for byes const hasOddTeams = teamPairings.length % 2 !== 0 const workingTeams = hasOddTeams ? [...teamPairings, { player1Id: -1, player2Id: -1 }] : [...teamPairings] const n = workingTeams.length const numRounds = n - 1 const matchupsPerRound = n / 2 const rounds: RoundSchedule[] = [] for (let round = 0; round < numRounds; round++) { const matchups: MatchupPairing[] = [] for (let i = 0; i < matchupsPerRound; i++) { const team1Idx = i const team2Idx = n - 1 - i const team1 = workingTeams[team1Idx] const team2 = workingTeams[team2Idx] // Skip bye matchups (where either team is the sentinel) if (team1.player1Id !== -1 && team2.player1Id !== -1) { matchups.push({ player1P1Id: team1.player1Id, player1P2Id: team1.player2Id, player2P1Id: team2.player1Id, player2P2Id: team2.player2Id, }) } } rounds.push({ roundNumber: round + 1, matchups }) // Rotate all teams except the first one (clockwise) // Move last element to position 1 const last = workingTeams.pop()! workingTeams.splice(1, 0, last) } return rounds } /** * Validate that a set of player pairings can be scheduled. */ export function validateScheduleInput( teamPairings: { player1Id: number; player2Id: number }[] ): { valid: boolean error?: string } { if (teamPairings.length < 2) { return { valid: false, error: "At least 2 teams are required to generate a schedule" } } // Check for duplicate teams const teamKeys = teamPairings.map( (t) => [t.player1Id, t.player2Id].sort().join('-') ) const uniqueKeys = new Set(teamKeys) if (uniqueKeys.size !== teamPairings.length) { return { valid: false, error: "Duplicate team pairings found" } } return { valid: true } } /** * Calculate the expected number of rounds for N teams. */ export function expectedRounds(teamCount: number): number { if (teamCount < 2) return 0 return teamCount % 2 === 0 ? teamCount - 1 : teamCount } /** * Calculate the expected number of total matchups for N teams. */ export function expectedMatchups(teamCount: number): number { if (teamCount < 2) return 0 return (teamCount * (teamCount - 1)) / 2 } /** * Generate a schedule where teams are created fresh each round. * This ensures every player gets different partners throughout the tournament. * * @param players - Array of players to be paired * @param teamCount - Number of teams (player pairs) to create per round * @param roundCount - Number of rounds to generate * @param generateTeamFunction - Function to generate teams for a round * @returns Array of rounds, each containing matchups with fresh team pairings */ export function generateVariableRoundRobin( players: { id: number; name: string; currentElo: number }[], teamCount: number, roundCount: number, generateTeamFunction: (players: { id: number; name: string; currentElo: number }[]) => TeamPairing[] ): RoundSchedule[] { if (players.length < 4 || teamCount < 2) { return [] } const rounds: RoundSchedule[] = [] for (let roundNum = 1; roundNum <= roundCount; roundNum++) { // Generate fresh teams for this round const teams = generateTeamFunction(players) // Validate we have enough teams if (teams.length < 2) { continue } // Apply round-robin pairing to the fresh teams const teamPairings = teams.map((team) => ({ player1Id: team.player1Id, player2Id: team.player2Id, })) // Use circle method for this round's matchups const hasOddTeams = teamPairings.length % 2 !== 0 const workingTeams = hasOddTeams ? [...teamPairings, { player1Id: -1, player2Id: -1 }] : [...teamPairings] const n = workingTeams.length const matchupsPerRound = n / 2 const matchups: MatchupPairing[] = [] for (let i = 0; i < matchupsPerRound; i++) { const team1Idx = i const team2Idx = n - 1 - i const team1 = workingTeams[team1Idx] const team2 = workingTeams[team2Idx] // Skip bye matchups if (team1.player1Id !== -1 && team2.player1Id !== -1) { matchups.push({ player1P1Id: team1.player1Id, player1P2Id: team1.player2Id, player2P1Id: team2.player1Id, player2P2Id: team2.player2Id, }) } } rounds.push({ roundNumber: roundNum, matchups }) } return rounds }