regionals

This commit is contained in:
yohlo
2026-03-01 16:21:27 -06:00
parent f83a7d69c8
commit 6199afc687
14 changed files with 849 additions and 137 deletions

View File

@@ -5,7 +5,7 @@ import { logger } from "@/lib/logger";
import { z } from "zod";
import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
import brackets from "@/features/bracket/utils";
import { MatchInput } from "@/features/matches/types";
import { Match, MatchInput } from "@/features/matches/types";
import { serverEvents } from "@/lib/events/emitter";
import { superTokensFunctionMiddleware } from "@/utils/supertokens";
import { PlayerInfo } from "../players/types";
@@ -164,6 +164,189 @@ export const startMatch = createServerFn()
})
);
export const populateKnockoutBracket = createServerFn()
.inputValidator(z.object({
tournamentId: z.string(),
}))
.middleware([superTokensAdminFunctionMiddleware, serverFnLoggingMiddleware])
.handler(async ({ data: { tournamentId } }) =>
toServerResult(async () => {
const tournament = await pbAdmin.getTournament(tournamentId);
if (!tournament) {
throw new Error("Tournament not found");
}
if (!tournament.group_config) {
throw new Error("Tournament must have group_config");
}
return await populateKnockoutBracketInternal(tournamentId, tournament.group_config);
})
);
async function populateKnockoutBracketInternal(tournamentId: string, groupConfig: { num_groups: number; advance_per_group: number }) {
logger.info('Populating knockout bracket', { tournamentId });
const groups = await pbAdmin.getGroupsByTournament(tournamentId);
if (!groups || groups.length === 0) {
throw new Error("No groups found for tournament");
}
const qualifiedTeams: { teamId: string; groupOrder: number; rank: number }[] = [];
for (const group of groups) {
logger.info('Processing group', {
groupId: group.id,
groupOrder: group.order,
teamsCount: group.teams?.length,
teams: group.teams
});
const groupMatches = await pbAdmin.getMatchesByGroup(group.id);
const completedMatches = groupMatches.filter(m => m.status === "ended");
const standings = new Map<string, { teamId: string; wins: number; losses: number; cups_for: number; cups_against: number; cup_differential: number }>();
for (const team of group.teams || []) {
// group.teams can be either team objects or just team ID strings
const teamId = typeof team === 'string' ? team : team.id;
standings.set(teamId, {
teamId,
wins: 0,
losses: 0,
cups_for: 0,
cups_against: 0,
cup_differential: 0,
});
}
for (const match of completedMatches) {
if (!match.home || !match.away) continue;
const homeStanding = standings.get(match.home.id);
const awayStanding = standings.get(match.away.id);
if (homeStanding && awayStanding) {
homeStanding.cups_for += match.home_cups;
homeStanding.cups_against += match.away_cups;
awayStanding.cups_for += match.away_cups;
awayStanding.cups_against += match.home_cups;
if (match.home_cups > match.away_cups) {
homeStanding.wins++;
awayStanding.losses++;
} else {
awayStanding.wins++;
homeStanding.losses++;
}
homeStanding.cup_differential = homeStanding.cups_for - homeStanding.cups_against;
awayStanding.cup_differential = awayStanding.cups_for - awayStanding.cups_against;
}
}
const sortedStandings = Array.from(standings.values()).sort((a, b) => {
if (b.wins !== a.wins) return b.wins - a.wins;
if (b.cup_differential !== a.cup_differential) return b.cup_differential - a.cup_differential;
return b.cups_for - a.cups_for;
});
const topTeams = sortedStandings.slice(0, groupConfig.advance_per_group);
logger.info('Top teams from group', {
groupId: group.id,
topTeams: topTeams.map(t => ({ teamId: t.teamId, wins: t.wins, cupDiff: t.cup_differential }))
});
topTeams.forEach((standing, index) => {
qualifiedTeams.push({
teamId: standing.teamId,
groupOrder: group.order,
rank: index + 1,
});
});
}
logger.info('Qualified teams', { qualifiedTeams });
const orderedTeamIds: string[] = [];
const maxRank = groupConfig.advance_per_group;
for (let rank = 1; rank <= maxRank; rank++) {
const teamsAtRank = qualifiedTeams
.filter(t => t.rank === rank)
.sort((a, b) => a.groupOrder - b.groupOrder);
orderedTeamIds.push(...teamsAtRank.map(t => t.teamId));
}
logger.info('Ordered team IDs', { orderedTeamIds });
const tournament = await pbAdmin.getTournament(tournamentId);
const knockoutMatches = (tournament.matches || [])
.filter((m: Match) => m.round >= 0 && m.lid >= 0)
.sort((a: Match, b: Match) => a.lid - b.lid);
const seedToTeamId = new Map<number, string>();
orderedTeamIds.forEach((teamId, index) => {
seedToTeamId.set(index + 1, teamId);
});
logger.info('Seed to team mapping', {
seedToTeamId: Array.from(seedToTeamId.entries()),
orderedTeamIds
});
let updatedCount = 0;
for (const match of knockoutMatches) {
if (match.round === 0) {
const updates: any = {};
if (match.home_seed) {
const teamId = seedToTeamId.get(match.home_seed);
logger.info('Looking up home seed', {
matchId: match.id,
home_seed: match.home_seed,
teamId
});
if (teamId) {
updates.home = teamId;
}
}
if (match.away_seed) {
const teamId = seedToTeamId.get(match.away_seed);
logger.info('Looking up away seed', {
matchId: match.id,
away_seed: match.away_seed,
teamId
});
if (teamId) {
updates.away = teamId;
}
}
if (updates.home && updates.away) {
updates.status = "ready";
} else if (updates.home || updates.away) {
updates.status = "tbd";
}
if (Object.keys(updates).length > 0) {
logger.info('Updating match', { matchId: match.id, updates });
await pbAdmin.updateMatch(match.id, updates);
updatedCount++;
}
}
}
logger.info('Updated matches', { updatedCount, totalKnockoutMatches: knockoutMatches.length });
await pbAdmin.updateTournament(tournamentId, {
phase: "knockout"
});
logger.info('Knockout bracket populated successfully', { tournamentId });
}
const endMatchSchema = z.object({
matchId: z.string(),
home_cups: z.number(),
@@ -190,19 +373,25 @@ export const endMatch = createServerFn()
ot_count,
});
if (match.lid === -1) {
serverEvents.emit("match", {
type: "match",
matchId: match.id,
tournamentId: match.tournament.id
});
return match;
}
const matchWinner = home_cups > away_cups ? match.home : match.away;
const matchLoser = home_cups < away_cups ? match.home : match.away;
if (!matchWinner || !matchLoser) throw new Error("Something went wrong");
// winner -> where to send match winner to, loser same
const { winner, loser } = await pbAdmin.getChildMatches(matchId);
// reset match check
if (winner && winner.reset) {
const awayTeamWon = match.away === matchWinner;
if (!awayTeamWon) {
// Reset match is not necessary
logger.info("Deleting reset match", {
resetMatchId: winner.id,
currentMatchId: match.id,
@@ -214,7 +403,6 @@ export const endMatch = createServerFn()
}
}
// advance bracket
if (winner) {
await pbAdmin.updateMatch(winner.id, {
[winner.home_from_lid === match.lid ? "home" : "away"]: matchWinner.id,