/** * Draft Eligibility Calculation for Omni Leagues * * Implements two key constraints: * 1. Team Flex Pick Limit: Teams can only use (totalRounds - totalSports) flex picks * 2. Global Sport Availability: Cannot draft from a sport if it prevents other teams * from fulfilling their "at least one from each sport" requirement */ export interface SportAvailability { sportId: string; sportName: string; totalParticipants: number; draftedCount: number; undraftedCount: number; teamsStillNeeding: number; canDraftAsFirst: boolean; // undraftedCount >= teamsStillNeeding canDraftAsFlex: boolean; // undraftedCount > teamsStillNeeding } export type DraftIneligibilityCode = | "no_flex_spots" | "no_extra_participants" | "unavailable"; export interface DraftIneligibilityReason { code: DraftIneligibilityCode; message: string; } export interface DraftEligibility { teamId: string; eligibleSportIds: Set; ineligibleReasons: Record; // sportId -> reason flexPicksUsed: number; flexPicksAvailable: number; picksBySport: Record; sportAvailability: Record; } interface PickData { teamId: string; participant: { id: string; sport: { id: string; name: string; }; }; } interface ParticipantData { id: string; sport: { id: string; name: string; }; } interface SportData { id: string; name: string; } interface TeamData { id: string; } function getFlexSpotsMessage( flexPicksUsed: number, flexPicksAvailable: number ): DraftIneligibilityReason { return { code: "no_flex_spots", message: `Out of flex spots (${flexPicksUsed}/${flexPicksAvailable})`, }; } function getNoExtrasMessage(sportName: string): DraftIneligibilityReason { return { code: "no_extra_participants", message: `Not enough ${sportName} participants for remaining teams`, }; } function getUnavailableMessage( sportName: string, undraftedCount: number, teamsStillNeeding: number ): DraftIneligibilityReason { return { code: "unavailable", message: `Only ${undraftedCount} ${sportName} participant${undraftedCount === 1 ? "" : "s"} left for ${teamsStillNeeding} team${teamsStillNeeding === 1 ? "" : "s"}`, }; } /** * Calculate which sports a team is eligible to draft from based on: * - Team's flex pick usage * - Global sport availability for all teams */ export function calculateDraftEligibility( teamId: string, teamPicks: PickData[], allPicks: PickData[], allParticipants: ParticipantData[], seasonSports: SportData[], totalRounds: number, allTeams: TeamData[] ): DraftEligibility { // Step 1: Calculate team's flex pick status const picksBySport: Record = {}; for (const pick of teamPicks) { const sportId = pick.participant.sport.id; picksBySport[sportId] = (picksBySport[sportId] || 0) + 1; } // Calculate flex picks used: sum of (picks - 1) for each sport with multiple picks let flexPicksUsed = 0; for (const sportId in picksBySport) { if (picksBySport[sportId] > 1) { flexPicksUsed += picksBySport[sportId] - 1; } } const totalSports = seasonSports.length; const flexPicksAvailable = totalRounds - totalSports; const hasFlexesRemaining = flexPicksUsed < flexPicksAvailable; // Step 2: Calculate global sport availability const sportAvailability: Record = {}; for (const sport of seasonSports) { // Count total participants in this sport const totalParticipants = allParticipants.filter( p => p.sport.id === sport.id ).length; // Count drafted participants in this sport const draftedCount = allPicks.filter( pick => pick.participant.sport.id === sport.id ).length; const undraftedCount = totalParticipants - draftedCount; // Count teams that haven't drafted from this sport yet const teamsSportPicks: Record = {}; for (const pick of allPicks) { if (pick.participant.sport.id === sport.id) { teamsSportPicks[pick.teamId] = (teamsSportPicks[pick.teamId] || 0) + 1; } } const teamsStillNeeding = allTeams.filter( team => !teamsSportPicks[team.id] || teamsSportPicks[team.id] === 0 ).length; sportAvailability[sport.id] = { sportId: sport.id, sportName: sport.name, totalParticipants, draftedCount, undraftedCount, teamsStillNeeding, canDraftAsFirst: undraftedCount >= teamsStillNeeding, canDraftAsFlex: undraftedCount > teamsStillNeeding, }; } // Step 3: Combine constraints to determine eligibility const eligibleSportIds = new Set(); const ineligibleReasons: Record = {}; for (const sport of seasonSports) { const sportId = sport.id; const availability = sportAvailability[sportId]; const currentTeamHasSport = (picksBySport[sportId] || 0) > 0; let isEligible = false; let reason: DraftIneligibilityReason | null = null; if (!currentTeamHasSport) { // Team hasn't drafted from this sport yet (would be first pick) if (availability.canDraftAsFirst) { isEligible = true; } else { reason = getUnavailableMessage(sport.name, availability.undraftedCount, availability.teamsStillNeeding); } } else { // Team has drafted from this sport (would be flex pick) if (!hasFlexesRemaining) { reason = getFlexSpotsMessage(flexPicksUsed, flexPicksAvailable); } else if (!availability.canDraftAsFlex) { reason = getNoExtrasMessage(sport.name); } else { isEligible = true; } } if (isEligible) { eligibleSportIds.add(sportId); } else if (reason) { ineligibleReasons[sportId] = reason; } } return { teamId, eligibleSportIds, ineligibleReasons, flexPicksUsed, flexPicksAvailable, picksBySport, sportAvailability, }; } /** * Get a human-readable summary of draft eligibility status */ export function getEligibilitySummary(eligibility: DraftEligibility): string { const { flexPicksUsed, flexPicksAvailable, picksBySport, sportAvailability } = eligibility; const sportBreakdown = Object.entries(picksBySport) .map(([sportId, count]) => { const sport = sportAvailability[sportId]; return `${sport?.sportName || sportId}: ${count}`; }) .join(", "); return `Flex picks: ${flexPicksUsed}/${flexPicksAvailable} | Picks by sport: ${sportBreakdown || "None"}`; }