starting to have a bracket system that is previewable
This commit is contained in:
parent
62127cc5e4
commit
82ecf80068
82 changed files with 3461 additions and 637 deletions
238
src/lib/server/tournament/double-round-robin.ts
Normal file
238
src/lib/server/tournament/double-round-robin.ts
Normal file
|
@ -0,0 +1,238 @@
|
|||
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<string[]> {
|
||||
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<RoundInsert[]> {
|
||||
// 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<Match[]> {
|
||||
// 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<RoundInsert | null> {
|
||||
// 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'
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue