brackt/app/lib/draft-eligibility.ts
Chris Parsons decb28dc19 Implement Omni League Draft Eligibility Rules
- Created draft eligibility calculation logic in `app/lib/draft-eligibility.ts`
- Added interfaces for sport availability and draft eligibility
- Implemented `calculateDraftEligibility` function to determine eligible sports based on team picks and global availability
- Developed `getEligibilitySummary` function for human-readable eligibility status
- Updated API endpoints to validate eligibility before making picks
- Enhanced frontend draft room UI to reflect eligibility status and provide visual indicators for ineligible participants
- Comprehensive implementation summary and testing documentation added
- All changes type-checked and unit tests passed successfully
2025-10-24 21:12:07 -07:00

195 lines
5.5 KiB
TypeScript

/**
* 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 interface DraftEligibility {
teamId: string;
eligibleSportIds: Set<string>;
ineligibleReasons: Record<string, string>; // sportId -> reason
flexPicksUsed: number;
flexPicksAvailable: number;
picksBySport: Record<string, number>;
sportAvailability: Record<string, SportAvailability>;
}
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;
}
/**
* 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<string, number> = {};
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<string, SportAvailability> = {};
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<string, number> = {};
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<string>();
const ineligibleReasons: Record<string, string> = {};
for (const sport of seasonSports) {
const sportId = sport.id;
const availability = sportAvailability[sportId];
const currentTeamHasSport = (picksBySport[sportId] || 0) > 0;
let isEligible = false;
let reason = "";
if (!currentTeamHasSport) {
// Team hasn't drafted from this sport yet (would be first pick)
if (availability.canDraftAsFirst) {
isEligible = true;
} else {
reason = `Only ${availability.undraftedCount} participants left for ${availability.teamsStillNeeding} teams needing ${sport.name}`;
}
} else {
// Team has drafted from this sport (would be flex pick)
if (!hasFlexesRemaining) {
reason = `All flex picks used (${flexPicksUsed}/${flexPicksAvailable})`;
} else if (!availability.canDraftAsFlex) {
reason = `Would prevent other teams from getting ${sport.name} (${availability.undraftedCount} left for ${availability.teamsStillNeeding} teams)`;
} else {
isEligible = true;
}
}
if (isEligible) {
eligibleSportIds.add(sportId);
} else {
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"}`;
}