import { db } from '../db'; import { brackets } from '../db/schema/brackets'; import { rounds } from '../db/schema/rounds'; import { matches, type Match } from '../db/schema/matches'; import { BracketType, MatchStatus, SchedulingMode } from '@/types'; import { BaseTournamentGenerator } from './types'; import type { RoundInsert } from '../db/schema/rounds'; import { v7 } from 'uuid'; import { eq } from 'drizzle-orm'; import { fields } from '../db/schema/fields'; import type { Team } from '../db/schema/teams'; export class DoubleRoundRobinGenerator extends BaseTournamentGenerator { getMode(): SchedulingMode { return SchedulingMode.double_round_robin; } /** * Generate a single main bracket for double round robin */ async generateBrackets(): Promise { const [bracket] = await db .insert(brackets) .values({ id: v7(), name: 'Main Bracket', bracketType: BracketType.MAIN, position: 1, isActive: true, competition_id: this.competitionId, scheduling_mode: SchedulingMode.double_round_robin }) .returning(); return [bracket.id]; } /** * Generate all rounds for a double round robin tournament * In double round robin, each team plays against every other team twice * (once home, once away) */ async generateRounds(bracketId: string): Promise { // Number of rounds needed = 2 * (n-1) where n is the number of teams const totalRounds = 2 * (this.teamsCount - 1); const createdRounds: RoundInsert[] = []; for (let i = 0; i < totalRounds; i++) { const roundNumber = i + 1; const roundName = `Round ${roundNumber}`; // Each round has floor(n/2) matches const matchesInRound = Math.floor(this.teamsCount / 2); const roundData: RoundInsert = { name: roundName, round_number: roundNumber, nb_matches: matchesInRound, bracket_id: bracketId }; const [insertedRound] = await db.insert(rounds).values(roundData).returning(); createdRounds.push(insertedRound); } return createdRounds; } /** * Generate matches for a double round robin tournament * This extends the round robin algorithm to create two sets of matches * with home/away teams reversed in the second half */ async generateMatches(roundId: string, teams: Team[]): Promise { // Get the round const [round] = await db.select().from(rounds).where(eq(rounds.id, roundId)); if (!round) { throw new Error(`Round not found: ${roundId}`); } if (!round.bracket_id) { throw new Error(`Round ${roundId} has no bracket`); } // Get the bracket const [bracket] = await db.select().from(brackets).where(eq(brackets.id, round.bracket_id)); if (!bracket) { throw new Error(`Bracket not found for round: ${roundId}`); } // Default field (could be enhanced to assign different fields) const [defaultField] = await db .select() .from(fields) .where(eq(fields.competition_id, this.competitionId)) .limit(1); if (!defaultField) { throw new Error('No field available for matches'); } // Generate pairings using the circle method const singleRoundRobinRounds = this.teamsCount - 1; const isSecondHalf = round.round_number > singleRoundRobinRounds; // For second half, we need to flip home/away teams const effectiveRoundNumber = isSecondHalf ? round.round_number - singleRoundRobinRounds : round.round_number; // Generate pairings for this round const pairings = this.generateRoundRobinPairings(teams, effectiveRoundNumber); // Create match objects const matchesToCreate: Match[] = pairings.map((pairing, index) => { const now = new Date(); const endTime = new Date(now); endTime.setHours(endTime.getHours() + 1); // For the second half of matches, swap team1 and team2 to reverse home/away const [team1Id, team2Id] = isSecondHalf ? [pairing.team2Id, pairing.team1Id] : [pairing.team1Id, pairing.team2Id]; return { id: v7(), match_number: index + 1, team1_id: team1Id, team2_id: team2Id, start_date: now, end_date: endTime, position: index + 1, table: index + 1, // Table assignment round_id: roundId, bracket_id: bracket.id, field_id: defaultField.id, score1: 0, score2: 0, status: MatchStatus.PENDING, winner_id: null, created_at: now, updated_at: null, deleted_at: null }; }); // Insert matches and return them const createdMatches = await db.insert(matches).values(matchesToCreate).returning(); return createdMatches; } /** * In double round robin, there's no "next round" generation based on results * All rounds are created at the beginning */ async generateNextRound(previousRoundId: string, bracketId: string): Promise { // Get the previous round const [previousRound] = await db.select().from(rounds).where(eq(rounds.id, previousRoundId)); if (!previousRound) { throw new Error(`Previous round not found: ${previousRoundId}`); } // Get all rounds in the bracket const allRounds = await db .select() .from(rounds) .where(eq(rounds.bracket_id, bracketId)) .orderBy(rounds.round_number); // Check if this was the final round if (previousRound.round_number === allRounds.length) { return null; // Tournament is complete } // Find the next round const nextRound = allRounds.find((r) => r.round_number === previousRound.round_number + 1); if (!nextRound) { throw new Error(`Next round not found for round ${previousRound.round_number}`); } return nextRound; } /** * Generate pairings for a round robin tournament using the circle method * In this method, one team stays fixed (team at index 0) and others rotate clockwise */ private generateRoundRobinPairings(teams: Team[], roundNumber: number) { // If odd number of teams, add a "bye" team if (teams.length % 2 !== 0) { teams.push({ id: '00000000-0000-0000-0000-000000000000', name: '', created_at: new Date(Date.now()), updated_at: null, deleted_at: null, competition_id: '' }); // Bye team ID } const n = teams.length; const pairings: { team1Id: string; team2Id: string }[] = []; // Create a copy of the teams array that we can manipulate const teamsForRound = [...teams]; // Rotate the teams (except the first one) based on the round number // For round 1, use teams as is // For subsequent rounds, rotate teams[1...n-1] clockwise if (roundNumber > 1) { // Apply rotation (roundNumber - 1) times for (let i = 0; i < roundNumber - 1; i++) { const lastTeam = teamsForRound.pop()!; teamsForRound.splice(1, 0, lastTeam); } } // Create pairings for this round for (let i = 0; i < n / 2; i++) { // Match teams[i] with teams[n-1-i] pairings.push({ team1Id: teamsForRound[i].id, team2Id: teamsForRound[n - 1 - i].id }); } // Filter out pairings with the bye team return pairings.filter( (pairing) => pairing.team1Id !== '00000000-0000-0000-0000-000000000000' && pairing.team2Id !== '00000000-0000-0000-0000-000000000000' ); } }