starting to have a bracket system that is previewable

This commit is contained in:
unurled 2025-07-14 20:48:29 +02:00
parent 62127cc5e4
commit 82ecf80068
Signed by: unurled
GPG key ID: EFC5F5E709B47DDD
82 changed files with 3461 additions and 637 deletions

View 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'
);
}
}