2025-10-24 21:12:07 -07:00
/ * *
* 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
}
2026-05-04 21:54:14 -07:00
export type DraftIneligibilityCode =
| "no_flex_spots"
| "no_extra_participants"
| "unavailable" ;
export interface DraftIneligibilityReason {
code : DraftIneligibilityCode ;
message : string ;
}
2025-10-24 21:12:07 -07:00
export interface DraftEligibility {
teamId : string ;
eligibleSportIds : Set < string > ;
2026-05-04 21:54:14 -07:00
ineligibleReasons : Record < string , DraftIneligibilityReason > ; // sportId -> reason
2025-10-24 21:12:07 -07:00
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 ;
}
2026-05-04 21:54:14 -07:00
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" } ` ,
} ;
}
2025-10-24 21:12:07 -07:00
/ * *
* 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 > ( ) ;
2026-05-04 21:54:14 -07:00
const ineligibleReasons : Record < string , DraftIneligibilityReason > = { } ;
2025-10-24 21:12:07 -07:00
for ( const sport of seasonSports ) {
const sportId = sport . id ;
const availability = sportAvailability [ sportId ] ;
const currentTeamHasSport = ( picksBySport [ sportId ] || 0 ) > 0 ;
let isEligible = false ;
2026-05-04 21:54:14 -07:00
let reason : DraftIneligibilityReason | null = null ;
2025-10-24 21:12:07 -07:00
if ( ! currentTeamHasSport ) {
// Team hasn't drafted from this sport yet (would be first pick)
if ( availability . canDraftAsFirst ) {
isEligible = true ;
} else {
2026-05-04 21:54:14 -07:00
reason = getUnavailableMessage ( sport . name , availability . undraftedCount , availability . teamsStillNeeding ) ;
2025-10-24 21:12:07 -07:00
}
} else {
// Team has drafted from this sport (would be flex pick)
if ( ! hasFlexesRemaining ) {
2026-05-04 21:54:14 -07:00
reason = getFlexSpotsMessage ( flexPicksUsed , flexPicksAvailable ) ;
2025-10-24 21:12:07 -07:00
} else if ( ! availability . canDraftAsFlex ) {
2026-05-04 21:54:14 -07:00
reason = getNoExtrasMessage ( sport . name ) ;
2025-10-24 21:12:07 -07:00
} else {
isEligible = true ;
}
}
if ( isEligible ) {
eligibleSportIds . add ( sportId ) ;
2026-05-04 21:54:14 -07:00
} else if ( reason ) {
2025-10-24 21:12:07 -07:00
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" } ` ;
}