- 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
232 lines
7.3 KiB
TypeScript
232 lines
7.3 KiB
TypeScript
import { database } from "~/database/context";
|
|
import * as schema from "~/database/schema";
|
|
import { eq, and, notInArray, desc, inArray } from "drizzle-orm";
|
|
import { getTeamQueue } from "./draft-queue";
|
|
import { isParticipantDrafted, getDraftPicksWithSports, getTeamDraftPicksWithSports } from "./draft-pick";
|
|
import { getParticipantsForSeasonWithSports } from "./participant";
|
|
import { getSeasonSportsSimple } from "./season-sport";
|
|
import { calculateDraftEligibility } from "~/lib/draft-eligibility";
|
|
|
|
/**
|
|
* Auto-pick for a team when their timer runs out
|
|
* 1. Check queue - pick first eligible item if available
|
|
* 2. If queue empty, pick highest EV participant not drafted from eligible sports
|
|
*
|
|
* Updated to respect Omni league draft eligibility rules
|
|
*/
|
|
export async function autoPickForTeam(
|
|
seasonId: string,
|
|
teamId: string,
|
|
draftRounds: number,
|
|
allTeamIds: string[]
|
|
) {
|
|
const db = database();
|
|
|
|
// Calculate eligibility for this team
|
|
const allPicks = await getDraftPicksWithSports(seasonId);
|
|
const teamPicks = await getTeamDraftPicksWithSports(teamId, seasonId);
|
|
const allParticipants = await getParticipantsForSeasonWithSports(seasonId);
|
|
const seasonSports = await getSeasonSportsSimple(seasonId);
|
|
const allTeams = allTeamIds.map((id) => ({ id }));
|
|
|
|
const eligibility = calculateDraftEligibility(
|
|
teamId,
|
|
teamPicks,
|
|
allPicks,
|
|
allParticipants,
|
|
seasonSports,
|
|
draftRounds,
|
|
allTeams
|
|
);
|
|
|
|
// Check queue first - filter by eligible sports
|
|
const queue = await getTeamQueue(teamId);
|
|
|
|
if (queue.length > 0) {
|
|
// Get participant details for queue items to check sport eligibility
|
|
const queueParticipantIds = queue.map((item) => item.participantId);
|
|
const queueParticipants = await db.query.participants.findMany({
|
|
where: inArray(schema.participants.id, queueParticipantIds),
|
|
with: {
|
|
sportsSeason: {
|
|
with: {
|
|
sport: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
// Try queue items in order, checking both drafted status and eligibility
|
|
for (const item of queue) {
|
|
const participant = queueParticipants.find((p) => p.id === item.participantId);
|
|
if (!participant) continue;
|
|
|
|
const sportId = participant.sportsSeason.sport.id;
|
|
const isEligible = eligibility.eligibleSportIds.has(sportId);
|
|
const isDrafted = await isParticipantDrafted(seasonId, item.participantId);
|
|
|
|
if (!isDrafted && isEligible) {
|
|
return item.participantId;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Queue is empty or all queued players drafted/ineligible
|
|
// Pick highest EV available from eligible sports
|
|
return await getTopAvailableParticipant(seasonId, eligibility.eligibleSportIds);
|
|
}
|
|
|
|
/**
|
|
* Get the highest EV participant that hasn't been drafted yet
|
|
* Updated to filter by eligible sports
|
|
*/
|
|
export async function getTopAvailableParticipant(
|
|
seasonId: string,
|
|
eligibleSportIds?: Set<string>
|
|
) {
|
|
const db = database();
|
|
|
|
// Get all drafted participant IDs
|
|
const draftedPicks = await db
|
|
.select({ participantId: schema.draftPicks.participantId })
|
|
.from(schema.draftPicks)
|
|
.where(eq(schema.draftPicks.seasonId, seasonId));
|
|
|
|
const draftedIds = draftedPicks.map((p: { participantId: string }) => p.participantId);
|
|
|
|
// Get all participants from season sports, filtered by eligible sports if provided
|
|
let seasonSportsData;
|
|
if (eligibleSportIds && eligibleSportIds.size > 0) {
|
|
// Filter to only eligible sports
|
|
seasonSportsData = await db
|
|
.select({
|
|
sportsSeasonId: schema.seasonSports.sportsSeasonId,
|
|
sportId: schema.sports.id,
|
|
})
|
|
.from(schema.seasonSports)
|
|
.innerJoin(
|
|
schema.sportsSeasons,
|
|
eq(schema.seasonSports.sportsSeasonId, schema.sportsSeasons.id)
|
|
)
|
|
.innerJoin(
|
|
schema.sports,
|
|
eq(schema.sportsSeasons.sportId, schema.sports.id)
|
|
)
|
|
.where(eq(schema.seasonSports.seasonId, seasonId));
|
|
|
|
// Filter to only eligible sports
|
|
seasonSportsData = seasonSportsData.filter((s: { sportId: string }) =>
|
|
eligibleSportIds.has(s.sportId)
|
|
);
|
|
} else {
|
|
// No filtering - get all sports
|
|
seasonSportsData = await db
|
|
.select({ sportsSeasonId: schema.seasonSports.sportsSeasonId })
|
|
.from(schema.seasonSports)
|
|
.where(eq(schema.seasonSports.seasonId, seasonId));
|
|
}
|
|
|
|
const sportsSeasonIds = seasonSportsData.map(
|
|
(s: { sportsSeasonId: string }) => s.sportsSeasonId
|
|
);
|
|
|
|
if (sportsSeasonIds.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
// Get top available participant by EV
|
|
let query = db
|
|
.select()
|
|
.from(schema.participants)
|
|
.where(eq(schema.participants.sportsSeasonId, sportsSeasonIds[0]))
|
|
.orderBy(desc(schema.participants.expectedValue), schema.participants.name);
|
|
|
|
// Filter out drafted participants if any exist
|
|
if (draftedIds.length > 0) {
|
|
query = db
|
|
.select()
|
|
.from(schema.participants)
|
|
.where(
|
|
and(
|
|
eq(schema.participants.sportsSeasonId, sportsSeasonIds[0]),
|
|
notInArray(schema.participants.id, draftedIds)
|
|
)
|
|
)
|
|
.orderBy(desc(schema.participants.expectedValue), schema.participants.name);
|
|
}
|
|
|
|
// Handle multiple sports seasons
|
|
if (sportsSeasonIds.length > 1) {
|
|
// For simplicity, we'll query all and sort in memory
|
|
// In production, might want to optimize this
|
|
const allParticipants = [];
|
|
|
|
for (const sportsSeasonId of sportsSeasonIds) {
|
|
let participantQuery = db
|
|
.select()
|
|
.from(schema.participants)
|
|
.where(eq(schema.participants.sportsSeasonId, sportsSeasonId));
|
|
|
|
if (draftedIds.length > 0) {
|
|
participantQuery = db
|
|
.select()
|
|
.from(schema.participants)
|
|
.where(
|
|
and(
|
|
eq(schema.participants.sportsSeasonId, sportsSeasonId),
|
|
notInArray(schema.participants.id, draftedIds)
|
|
)
|
|
);
|
|
}
|
|
|
|
const seasonParticipants = await participantQuery;
|
|
allParticipants.push(...seasonParticipants);
|
|
}
|
|
|
|
// Sort by EV desc, then name
|
|
allParticipants.sort((a, b) => {
|
|
if (b.expectedValue !== a.expectedValue) {
|
|
return b.expectedValue - a.expectedValue;
|
|
}
|
|
return a.name.localeCompare(b.name);
|
|
});
|
|
|
|
return allParticipants[0]?.id || null;
|
|
}
|
|
|
|
const [topParticipant] = await query;
|
|
return topParticipant?.id || null;
|
|
}
|
|
|
|
/**
|
|
* Calculate the current pick based on draft order and round
|
|
*/
|
|
export function calculatePickInfo(
|
|
pickNumber: number,
|
|
teamCount: number
|
|
): { round: number; pickInRound: number; teamIndex: number } {
|
|
const round = Math.ceil(pickNumber / teamCount);
|
|
const pickInRound = ((pickNumber - 1) % teamCount) + 1;
|
|
|
|
// Snake draft: odd rounds go forward, even rounds go backward
|
|
const isOddRound = round % 2 === 1;
|
|
const teamIndex = isOddRound ? pickInRound - 1 : teamCount - pickInRound;
|
|
|
|
return { round, pickInRound, teamIndex };
|
|
}
|
|
|
|
/**
|
|
* Get the team ID for a given pick number based on draft order
|
|
*/
|
|
export function getTeamForPick(
|
|
pickNumber: number,
|
|
draftOrder: { teamId: string; draftOrder: number }[]
|
|
): string | null {
|
|
const sortedOrder = [...draftOrder].sort((a, b) => a.draftOrder - b.draftOrder);
|
|
const teamCount = sortedOrder.length;
|
|
|
|
if (teamCount === 0) return null;
|
|
|
|
const { teamIndex } = calculatePickInfo(pickNumber, teamCount);
|
|
return sortedOrder[teamIndex]?.teamId || null;
|
|
}
|