import { Match } from "@/features/matches/types"; import { Team, TeamInfo } from "@/features/teams/types"; import { Tournament } from "@/features/tournaments/types"; import { PicksMap } from "./types"; const teamId = (team?: TeamInfo | Team | string): string | undefined => typeof team === "string" ? team : team?.id; const asTeamInfo = (team?: TeamInfo | Team | string): TeamInfo | undefined => typeof team === "string" ? undefined : team; export interface ResolvedMatch { lid: number; home?: TeamInfo; homeId?: string; away?: TeamInfo; awayId?: string; pickedWinner?: TeamInfo; pickedWinnerId?: string; pickedLoser?: TeamInfo; pickedLoserId?: string; resetNecessary?: boolean; } const isBracketMatch = (match: Match) => match.round !== -1 && !match.bye; export const getPickableMatches = ( matches: Match[], picks: PicksMap ): Match[] => { const resolved = resolvePredictedBracket(matches, picks); return matches .filter( (match) => isBracketMatch(match) && (!match.reset || resolved.get(match.lid)?.resetNecessary) ) .sort((a, b) => a.lid - b.lid); }; export const isPredictionLocked = (matches: Match[]): boolean => matches.some( (match) => match.status === "started" || match.status === "ended" ); export const isTournamentPredictable = (tournament: Tournament): boolean => { const matches = tournament.matches || []; return ( !tournament.regional && matches.length > 0 && !matches.some((match) => match.round === -1) ); }; const resolveInternal = ( matches: Match[], picks: PicksMap, prune: boolean ): { resolved: Map; picks: PicksMap } => { const resolved = new Map(); const nextPicks: PicksMap = { ...picks }; const bracketMatches = matches .filter(isBracketMatch) .sort((a, b) => a.lid - b.lid); for (const match of bracketMatches) { const entry: ResolvedMatch = { lid: match.lid }; if (match.home_from_lid === -1) { entry.home = asTeamInfo(match.home); entry.homeId = teamId(match.home); } else { const source = resolved.get(match.home_from_lid); entry.home = match.home_from_loser ? source?.pickedLoser : source?.pickedWinner; entry.homeId = match.home_from_loser ? source?.pickedLoserId : source?.pickedWinnerId; } if (match.away_from_lid === -1) { entry.away = asTeamInfo(match.away); entry.awayId = teamId(match.away); } else { const source = resolved.get(match.away_from_lid); entry.away = match.away_from_loser ? source?.pickedLoser : source?.pickedWinner; entry.awayId = match.away_from_loser ? source?.pickedLoserId : source?.pickedWinnerId; } let pickEligible = true; if (match.reset) { const grandFinal = resolved.get(match.home_from_lid); entry.resetNecessary = !!grandFinal?.pickedWinnerId && grandFinal.pickedWinnerId === grandFinal.awayId; pickEligible = entry.resetNecessary; } const pickId = nextPicks[String(match.lid)]; if (pickEligible && pickId && pickId === entry.homeId) { entry.pickedWinner = entry.home; entry.pickedWinnerId = entry.homeId; entry.pickedLoser = entry.away; entry.pickedLoserId = entry.awayId; } else if (pickEligible && pickId && pickId === entry.awayId) { entry.pickedWinner = entry.away; entry.pickedWinnerId = entry.awayId; entry.pickedLoser = entry.home; entry.pickedLoserId = entry.homeId; } else if (pickId && prune) { delete nextPicks[String(match.lid)]; } resolved.set(match.lid, entry); } return { resolved, picks: nextPicks }; }; export const resolvePredictedBracket = ( matches: Match[], picks: PicksMap ): Map => resolveInternal(matches, picks, false).resolved; export const setPick = ( matches: Match[], picks: PicksMap, lid: number, pickedTeamId: string ): PicksMap => resolveInternal( matches, { ...picks, [String(lid)]: pickedTeamId }, true ).picks; export const isPredictionComplete = ( matches: Match[], picks: PicksMap ): boolean => { const resolved = resolvePredictedBracket(matches, picks); return getPickableMatches(matches, picks).every( (match) => resolved.get(match.lid)?.pickedWinnerId ); }; export const getMatchLabel = (matches: Match[], match: Match): string => { if (match.reset) return "Bracket Reset"; const winners = matches.filter( (m) => isBracketMatch(m) && !m.reset && !m.is_losers_bracket ); const grandFinal = winners.reduce( (highest: Match | undefined, current) => !highest || current.lid > highest.lid ? current : highest, undefined ); if (!grandFinal) return `Match ${match.order}`; const hasLosersBracket = matches.some( (m) => isBracketMatch(m) && m.is_losers_bracket ); if (match.lid === grandFinal.lid) return "Final"; if ( hasLosersBracket && !match.is_losers_bracket && grandFinal.home_from_lid === match.lid ) { return "Winners Bracket Final"; } if (match.is_losers_bracket && grandFinal.away_from_lid === match.lid) { return "Losers Bracket Final"; } return `Match ${match.order}`; }; export type PickResult = "correct" | "incorrect" | "pending"; export interface PredictionScore { points: number; correct: number; total: number; perMatch: Map; predictedChampionId?: string; } export const getMatchPoints = (match: Match): number => (match.is_losers_bracket ? 5 : 10) * 2 ** match.round; export const computePredictionScore = ( matches: Match[], picks: PicksMap ): PredictionScore => { const pickable = getPickableMatches(matches, picks); const perMatch = new Map(); let points = 0; let correct = 0; for (const match of pickable) { const pickId = picks[String(match.lid)]; if (match.status !== "ended") { perMatch.set(match.lid, "pending"); continue; } const actualWinnerId = match.home_cups > match.away_cups ? teamId(match.home) : teamId(match.away); if (pickId && actualWinnerId && pickId === actualWinnerId) { perMatch.set(match.lid, "correct"); points += getMatchPoints(match); correct += 1; } else { perMatch.set(match.lid, "incorrect"); } } const grandFinal = pickable .filter((match) => !match.is_losers_bracket) .at(-1); return { points, correct, total: pickable.length, perMatch, predictedChampionId: grandFinal ? picks[String(grandFinal.lid)] : undefined, }; };