feat: Refactor autopick logic to use unified executeAutoPick function and enhance eligibility checks
This commit is contained in:
parent
32980428f2
commit
714ab0f484
5 changed files with 396 additions and 407 deletions
|
|
@ -9,7 +9,7 @@ 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
|
||||
* 1. Check queue - pick first eligible item if available (cleans up ineligible items)
|
||||
* 2. If queue empty, pick highest EV participant not drafted from eligible sports
|
||||
*
|
||||
* Updated to respect Omni league draft eligibility rules
|
||||
|
|
@ -39,11 +39,18 @@ export async function autoPickForTeam(
|
|||
allTeams
|
||||
);
|
||||
|
||||
console.log(
|
||||
`[AutoPick] Team ${teamId} eligible sports:`,
|
||||
Array.from(eligibility.eligibleSportIds)
|
||||
);
|
||||
|
||||
// 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
|
||||
console.log(`[AutoPick] Team ${teamId} has ${queue.length} items in queue`);
|
||||
|
||||
// Get participant details for queue items to check eligibility
|
||||
const queueParticipantIds = queue.map((item) => item.participantId);
|
||||
const queueParticipants = await db.query.participants.findMany({
|
||||
where: inArray(schema.participants.id, queueParticipantIds),
|
||||
|
|
@ -56,23 +63,67 @@ export async function autoPickForTeam(
|
|||
},
|
||||
});
|
||||
|
||||
// Try queue items in order, checking both drafted status and eligibility
|
||||
const ineligibleQueueItemIds: string[] = [];
|
||||
|
||||
// Try queue items in order, checking both drafted status and sport eligibility
|
||||
for (const item of queue) {
|
||||
const participant = queueParticipants.find((p) => p.id === item.participantId);
|
||||
if (!participant) continue;
|
||||
if (!participant) {
|
||||
console.log(`[AutoPick] Queue item ${item.id} - participant not found, will remove`);
|
||||
ineligibleQueueItemIds.push(item.id);
|
||||
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;
|
||||
if (isDrafted) {
|
||||
console.log(
|
||||
`[AutoPick] Queue item ${participant.name} (${participant.sportsSeason.sport.name}) - already drafted, will remove`
|
||||
);
|
||||
ineligibleQueueItemIds.push(item.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isEligible) {
|
||||
console.log(
|
||||
`[AutoPick] Queue item ${participant.name} (${participant.sportsSeason.sport.name}) - not eligible for this team, will remove`
|
||||
);
|
||||
ineligibleQueueItemIds.push(item.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Found a valid pick from queue
|
||||
console.log(
|
||||
`[AutoPick] Selecting from queue: ${participant.name} (${participant.sportsSeason.sport.name})`
|
||||
);
|
||||
|
||||
// Clean up ineligible items from queue before returning
|
||||
if (ineligibleQueueItemIds.length > 0) {
|
||||
await db
|
||||
.delete(schema.draftQueue)
|
||||
.where(inArray(schema.draftQueue.id, ineligibleQueueItemIds));
|
||||
console.log(`[AutoPick] Removed ${ineligibleQueueItemIds.length} ineligible items from queue`);
|
||||
}
|
||||
|
||||
return item.participantId;
|
||||
}
|
||||
|
||||
// All queue items were ineligible or drafted - clean them up
|
||||
if (ineligibleQueueItemIds.length > 0) {
|
||||
await db
|
||||
.delete(schema.draftQueue)
|
||||
.where(inArray(schema.draftQueue.id, ineligibleQueueItemIds));
|
||||
console.log(
|
||||
`[AutoPick] Removed ${ineligibleQueueItemIds.length} ineligible items from queue (all items were invalid)`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Queue is empty or all queued players drafted/ineligible
|
||||
// Pick highest EV available from eligible sports
|
||||
console.log(`[AutoPick] No valid queue items, selecting highest EV from eligible sports`);
|
||||
return await getTopAvailableParticipant(seasonId, eligibility.eligibleSportIds);
|
||||
}
|
||||
|
||||
|
|
@ -224,9 +275,311 @@ export function getTeamForPick(
|
|||
): 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an autopick for a team - unified function for both commissioner-forced and timer-based autopicks
|
||||
*
|
||||
* Selection Logic:
|
||||
* 1. Uses the team's draft queue first (prioritizes manager's preferences)
|
||||
* 2. Validates each queued participant for:
|
||||
* - Not already drafted
|
||||
* - Eligible based on draft rules (sport eligibility, flex spots, etc.)
|
||||
* 3. Automatically removes ineligible participants from queue
|
||||
* 4. If queue is empty or all items are ineligible, selects highest EV participant from eligible sports
|
||||
*
|
||||
* This ensures autopicks ALWAYS respect draft eligibility rules and cannot make illegal selections.
|
||||
*
|
||||
* @param params.seasonId - The season ID
|
||||
* @param params.teamId - The team making the pick
|
||||
* @param params.pickNumber - The current pick number
|
||||
* @param params.triggeredBy - Who/what triggered the autopick ("commissioner" or "timer")
|
||||
* @param params.commissionerUserId - User ID of commissioner (required if triggeredBy is "commissioner")
|
||||
* @param params.autodraftSettings - Autodraft settings (used for timer-based picks)
|
||||
* @param params.db - Database instance (optional, will use database() if not provided)
|
||||
* @returns Result object with success status and pick data
|
||||
*/
|
||||
export async function executeAutoPick(params: {
|
||||
seasonId: string;
|
||||
teamId: string;
|
||||
pickNumber: number;
|
||||
triggeredBy: "commissioner" | "timer";
|
||||
commissionerUserId?: string;
|
||||
autodraftSettings?: any;
|
||||
db?: ReturnType<typeof database>;
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
pick?: any;
|
||||
participant?: any;
|
||||
nextPickNumber?: number;
|
||||
isDraftComplete?: boolean;
|
||||
}> {
|
||||
const {
|
||||
seasonId,
|
||||
teamId,
|
||||
pickNumber,
|
||||
triggeredBy,
|
||||
commissionerUserId,
|
||||
autodraftSettings,
|
||||
db: providedDb,
|
||||
} = params;
|
||||
|
||||
const db = providedDb || database();
|
||||
|
||||
try {
|
||||
// Race condition protection - check if pick already made
|
||||
const existingPick = await db.query.draftPicks.findFirst({
|
||||
where: and(
|
||||
eq(schema.draftPicks.seasonId, seasonId),
|
||||
eq(schema.draftPicks.pickNumber, pickNumber)
|
||||
),
|
||||
});
|
||||
|
||||
if (existingPick) {
|
||||
console.log(`[AutoPick] Pick ${pickNumber} already made, skipping`);
|
||||
return {
|
||||
success: false,
|
||||
error: "Pick already made",
|
||||
};
|
||||
}
|
||||
|
||||
// Get season details
|
||||
const season = await db.query.seasons.findFirst({
|
||||
where: eq(schema.seasons.id, seasonId),
|
||||
});
|
||||
|
||||
if (!season) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Season not found",
|
||||
};
|
||||
}
|
||||
|
||||
// Get draft slots to calculate round/pickInRound and get all team IDs
|
||||
const draftSlots = await db.query.draftSlots.findMany({
|
||||
where: eq(schema.draftSlots.seasonId, seasonId),
|
||||
orderBy: schema.draftSlots.draftOrder,
|
||||
with: {
|
||||
team: true,
|
||||
},
|
||||
});
|
||||
|
||||
const totalTeams = draftSlots.length;
|
||||
if (totalTeams === 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: "No draft slots found",
|
||||
};
|
||||
}
|
||||
|
||||
const allTeamIds = draftSlots.map((slot) => slot.teamId);
|
||||
|
||||
// Use autoPickForTeam to select participant (respects eligibility and queue)
|
||||
const participantId = await autoPickForTeam(
|
||||
seasonId,
|
||||
teamId,
|
||||
season.draftRounds,
|
||||
allTeamIds
|
||||
);
|
||||
|
||||
if (!participantId) {
|
||||
return {
|
||||
success: false,
|
||||
error: "No eligible participants available to pick",
|
||||
};
|
||||
}
|
||||
|
||||
// Get participant details
|
||||
const participantToPick = await db.query.participants.findFirst({
|
||||
where: eq(schema.participants.id, participantId),
|
||||
with: {
|
||||
sportsSeason: {
|
||||
with: {
|
||||
sport: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!participantToPick) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Participant not found",
|
||||
};
|
||||
}
|
||||
|
||||
// Calculate round and pickInRound with snake draft logic
|
||||
const currentRound = Math.ceil(pickNumber / totalTeams);
|
||||
const isEvenRound = currentRound % 2 === 0;
|
||||
let pickInRound = ((pickNumber - 1) % totalTeams) + 1;
|
||||
|
||||
// Apply snake draft reversal for even rounds
|
||||
if (isEvenRound) {
|
||||
pickInRound = totalTeams - pickInRound + 1;
|
||||
}
|
||||
|
||||
// Determine pickedByUserId based on trigger
|
||||
const pickedByUserId = triggeredBy === "commissioner"
|
||||
? (commissionerUserId || "")
|
||||
: "";
|
||||
|
||||
// Create the draft pick
|
||||
const [draftPick] = await db
|
||||
.insert(schema.draftPicks)
|
||||
.values({
|
||||
seasonId,
|
||||
teamId,
|
||||
participantId: participantToPick.id,
|
||||
pickNumber,
|
||||
round: currentRound,
|
||||
pickInRound,
|
||||
pickedByUserId,
|
||||
pickedByType: "auto",
|
||||
timeUsed: triggeredBy === "timer" ? (season.draftInitialTime || 120) : undefined,
|
||||
})
|
||||
.returning();
|
||||
|
||||
console.log(
|
||||
`[AutoPick] Pick created - ${triggeredBy} triggered - Pick ${pickNumber} - Participant ${participantId}`
|
||||
);
|
||||
|
||||
// Update season's current pick number and check if draft complete
|
||||
const nextPickNumber = pickNumber + 1;
|
||||
const totalPicks = totalTeams * season.draftRounds;
|
||||
const isDraftComplete = nextPickNumber > totalPicks;
|
||||
|
||||
await db
|
||||
.update(schema.seasons)
|
||||
.set({
|
||||
currentPickNumber: isDraftComplete ? pickNumber : nextPickNumber,
|
||||
status: isDraftComplete ? "active" : season.status,
|
||||
})
|
||||
.where(eq(schema.seasons.id, seasonId));
|
||||
|
||||
// Add increment to the team that just picked
|
||||
const currentTimer = await db.query.draftTimers.findFirst({
|
||||
where: and(
|
||||
eq(schema.draftTimers.seasonId, seasonId),
|
||||
eq(schema.draftTimers.teamId, teamId)
|
||||
),
|
||||
});
|
||||
|
||||
if (currentTimer) {
|
||||
const incrementTime = season.draftIncrementTime || 30;
|
||||
const newTimeRemaining = currentTimer.timeRemaining + incrementTime;
|
||||
await db
|
||||
.update(schema.draftTimers)
|
||||
.set({
|
||||
timeRemaining: newTimeRemaining,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(schema.draftTimers.id, currentTimer.id));
|
||||
|
||||
// Emit timer update to all clients
|
||||
try {
|
||||
const io = (global as any).__socketIO;
|
||||
if (io) {
|
||||
io.to(`draft-${seasonId}`).emit("timer-update", {
|
||||
seasonId,
|
||||
teamId,
|
||||
timeRemaining: newTimeRemaining,
|
||||
currentPickNumber: pickNumber,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[AutoPick] Socket.IO timer-update error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from ALL team queues in this season (participant is now drafted)
|
||||
await db
|
||||
.delete(schema.draftQueue)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.draftQueue.seasonId, seasonId),
|
||||
eq(schema.draftQueue.participantId, participantToPick.id)
|
||||
)
|
||||
);
|
||||
|
||||
// Handle autodraft settings for timer-based picks with "next_pick" mode
|
||||
if (triggeredBy === "timer" && autodraftSettings?.isEnabled && autodraftSettings.mode === "next_pick") {
|
||||
console.log(`[AutoPick] Disabling autodraft for team ${teamId} after next_pick`);
|
||||
await db
|
||||
.update(schema.autodraftSettings)
|
||||
.set({
|
||||
isEnabled: false,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(schema.autodraftSettings.id, autodraftSettings.id));
|
||||
|
||||
// Emit autodraft-updated event
|
||||
try {
|
||||
const io = (global as any).__socketIO;
|
||||
if (io) {
|
||||
io.to(`draft-${seasonId}`).emit("autodraft-updated", {
|
||||
teamId,
|
||||
isEnabled: false,
|
||||
mode: autodraftSettings.mode,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[AutoPick] Socket.IO autodraft-updated error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Emit socket events
|
||||
try {
|
||||
const io = (global as any).__socketIO;
|
||||
if (io) {
|
||||
const team = draftSlots.find((slot) => slot.team.id === teamId)?.team;
|
||||
|
||||
// Emit participant-removed-from-queues event
|
||||
io.to(`draft-${seasonId}`).emit("participant-removed-from-queues", {
|
||||
participantId: participantToPick.id,
|
||||
});
|
||||
|
||||
// Emit pick-made event
|
||||
io.to(`draft-${seasonId}`).emit("pick-made", {
|
||||
pick: {
|
||||
...draftPick,
|
||||
team,
|
||||
participant: {
|
||||
...participantToPick,
|
||||
sport: participantToPick.sportsSeason.sport,
|
||||
},
|
||||
sport: participantToPick.sportsSeason.sport,
|
||||
},
|
||||
nextPickNumber: isDraftComplete ? pickNumber : nextPickNumber,
|
||||
isDraftComplete,
|
||||
});
|
||||
|
||||
// Emit draft-completed event if applicable
|
||||
if (isDraftComplete) {
|
||||
io.to(`draft-${seasonId}`).emit("draft-completed");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[AutoPick] Socket.IO events error:", error);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
pick: draftPick,
|
||||
participant: participantToPick,
|
||||
nextPickNumber: isDraftComplete ? pickNumber : nextPickNumber,
|
||||
isDraftComplete,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("[AutoPick] Error in executeAutoPick:", error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { getAuth } from "@clerk/react-router/server";
|
||||
import { database } from "~/database/context";
|
||||
import * as schema from "~/database/schema";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { autoPickForTeam } from "~/models/draft-utils";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { executeAutoPick } from "~/models/draft-utils";
|
||||
|
||||
export async function action(args: any) {
|
||||
const { request } = args;
|
||||
|
|
@ -24,7 +24,7 @@ export async function action(args: any) {
|
|||
|
||||
const db = database();
|
||||
|
||||
// Get season details
|
||||
// Get season details to verify commissioner permissions
|
||||
const season = await db.query.seasons.findFirst({
|
||||
where: eq(schema.seasons.id, seasonId),
|
||||
with: {
|
||||
|
|
@ -41,166 +41,25 @@ export async function action(args: any) {
|
|||
return Response.json({ error: "Only commissioner can force autopick" }, { status: 403 });
|
||||
}
|
||||
|
||||
// Get draft slots to pass team IDs
|
||||
const draftSlots = await db.query.draftSlots.findMany({
|
||||
where: eq(schema.draftSlots.seasonId, seasonId),
|
||||
orderBy: schema.draftSlots.draftOrder,
|
||||
with: {
|
||||
team: true,
|
||||
},
|
||||
});
|
||||
|
||||
const allTeamIds = draftSlots.map((slot) => slot.teamId);
|
||||
|
||||
// Use autoPickForTeam which now respects eligibility
|
||||
const participantId = await autoPickForTeam(
|
||||
// Execute the autopick using the unified function
|
||||
const result = await executeAutoPick({
|
||||
seasonId,
|
||||
teamId,
|
||||
season.draftRounds,
|
||||
allTeamIds
|
||||
);
|
||||
|
||||
if (!participantId) {
|
||||
return Response.json({ error: "No eligible participants available to pick" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get participant details
|
||||
const participantToPick = await db.query.participants.findFirst({
|
||||
where: eq(schema.participants.id, participantId),
|
||||
with: {
|
||||
sportsSeason: {
|
||||
with: {
|
||||
sport: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
pickNumber,
|
||||
triggeredBy: "commissioner",
|
||||
commissionerUserId: userId,
|
||||
db,
|
||||
});
|
||||
|
||||
if (!participantToPick) {
|
||||
return Response.json({ error: "Participant not found" }, { status: 404 });
|
||||
if (!result.success) {
|
||||
return Response.json({ error: result.error }, { status: 400 });
|
||||
}
|
||||
|
||||
// Calculate round and pickInRound
|
||||
const totalTeams = draftSlots.length;
|
||||
const currentRound = Math.ceil(pickNumber / totalTeams);
|
||||
const isEvenRound = currentRound % 2 === 0;
|
||||
let pickInRound = ((pickNumber - 1) % totalTeams) + 1;
|
||||
if (isEvenRound) {
|
||||
pickInRound = totalTeams - pickInRound + 1;
|
||||
}
|
||||
|
||||
// Create the draft pick
|
||||
const [draftPick] = await db
|
||||
.insert(schema.draftPicks)
|
||||
.values({
|
||||
seasonId,
|
||||
teamId,
|
||||
participantId: participantToPick.id,
|
||||
pickNumber,
|
||||
round: currentRound,
|
||||
pickInRound,
|
||||
pickedByUserId: userId,
|
||||
pickedByType: "auto",
|
||||
})
|
||||
.returning();
|
||||
|
||||
// Update season's current pick number
|
||||
const nextPickNumber = pickNumber + 1;
|
||||
const totalPicks = totalTeams * season.draftRounds;
|
||||
const isDraftComplete = nextPickNumber > totalPicks;
|
||||
|
||||
await db
|
||||
.update(schema.seasons)
|
||||
.set({
|
||||
currentPickNumber: isDraftComplete ? pickNumber : nextPickNumber,
|
||||
status: isDraftComplete ? "active" : season.status,
|
||||
})
|
||||
.where(eq(schema.seasons.id, seasonId));
|
||||
|
||||
// Add increment to the team that just picked
|
||||
const currentTimer = await db.query.draftTimers.findFirst({
|
||||
where: and(
|
||||
eq(schema.draftTimers.seasonId, seasonId),
|
||||
eq(schema.draftTimers.teamId, teamId)
|
||||
),
|
||||
});
|
||||
|
||||
if (currentTimer) {
|
||||
const incrementTime = season.draftIncrementTime || 30;
|
||||
const newTimeRemaining = currentTimer.timeRemaining + incrementTime;
|
||||
await db
|
||||
.update(schema.draftTimers)
|
||||
.set({
|
||||
timeRemaining: newTimeRemaining,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(schema.draftTimers.id, currentTimer.id));
|
||||
|
||||
// Emit timer update to all clients
|
||||
try {
|
||||
const io = (global as any).__socketIO;
|
||||
io.to(`draft-${seasonId}`).emit("timer-update", {
|
||||
seasonId,
|
||||
teamId,
|
||||
timeRemaining: newTimeRemaining,
|
||||
currentPickNumber: pickNumber,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Socket.IO timer-update error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from ALL team queues in this season (participant is now drafted)
|
||||
await db
|
||||
.delete(schema.draftQueue)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.draftQueue.seasonId, seasonId),
|
||||
eq(schema.draftQueue.participantId, participantToPick.id)
|
||||
)
|
||||
);
|
||||
|
||||
// Notify all clients that this participant was removed from queues
|
||||
try {
|
||||
const io = (global as any).__socketIO;
|
||||
io.to(`draft-${seasonId}`).emit("participant-removed-from-queues", {
|
||||
participantId: participantToPick.id,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Socket.IO participant-removed-from-queues error:", error);
|
||||
}
|
||||
|
||||
// Emit socket event
|
||||
try {
|
||||
const io = (global as any).__socketIO;
|
||||
const team = draftSlots.find((slot) => slot.team.id === teamId)?.team;
|
||||
|
||||
io.to(`draft-${seasonId}`).emit("pick-made", {
|
||||
pick: {
|
||||
...draftPick,
|
||||
team,
|
||||
participant: {
|
||||
...participantToPick,
|
||||
sport: participantToPick.sportsSeason.sport,
|
||||
},
|
||||
sport: participantToPick.sportsSeason.sport,
|
||||
},
|
||||
nextPickNumber: isDraftComplete ? pickNumber : nextPickNumber,
|
||||
isDraftComplete,
|
||||
});
|
||||
|
||||
if (isDraftComplete) {
|
||||
io.to(`draft-${seasonId}`).emit("draft-completed");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Socket.IO error:", error);
|
||||
}
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
pick: draftPick,
|
||||
participant: participantToPick,
|
||||
nextPickNumber: isDraftComplete ? pickNumber : nextPickNumber,
|
||||
isDraftComplete,
|
||||
return Response.json({
|
||||
success: true,
|
||||
pick: result.pick,
|
||||
participant: result.participant,
|
||||
nextPickNumber: result.nextPickNumber,
|
||||
isDraftComplete: result.isDraftComplete,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
253
server/timer.ts
253
server/timer.ts
|
|
@ -1,8 +1,9 @@
|
|||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import postgres from "postgres";
|
||||
import * as schema from "~/database/schema";
|
||||
import { eq, and, desc, asc, inArray, notInArray } from "drizzle-orm";
|
||||
import { eq, and, asc } from "drizzle-orm";
|
||||
import { getSocketIO } from "./socket";
|
||||
import { executeAutoPick } from "~/models/draft-utils";
|
||||
|
||||
// Create a dedicated database connection for the timer
|
||||
const connectionString = process.env.DATABASE_URL;
|
||||
|
|
@ -186,6 +187,7 @@ async function updateDraftTimers(): Promise<void> {
|
|||
|
||||
/**
|
||||
* Trigger an automatic pick when timer expires
|
||||
* Uses the unified executeAutoPick function
|
||||
*/
|
||||
async function triggerAutoPick(
|
||||
seasonId: string,
|
||||
|
|
@ -194,254 +196,25 @@ async function triggerAutoPick(
|
|||
autodraftSettings: any | null
|
||||
): Promise<void> {
|
||||
try {
|
||||
const io = getSocketIO();
|
||||
console.log(
|
||||
`[Timer] Triggering auto-pick for team ${teamId} in season ${seasonId}, pick ${pickNumber}`
|
||||
);
|
||||
|
||||
// Get season details
|
||||
const season = await db.query.seasons.findFirst({
|
||||
where: eq(schema.seasons.id, seasonId),
|
||||
});
|
||||
|
||||
if (!season) {
|
||||
console.error(`[Timer] Season ${seasonId} not found for auto-pick`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if pick already made (race condition protection)
|
||||
const existingPick = await db.query.draftPicks.findFirst({
|
||||
where: and(
|
||||
eq(schema.draftPicks.seasonId, seasonId),
|
||||
eq(schema.draftPicks.pickNumber, pickNumber)
|
||||
),
|
||||
});
|
||||
|
||||
if (existingPick) {
|
||||
console.log(`[Timer] Pick ${pickNumber} already made, skipping auto-pick`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get team's queue
|
||||
const queueItems = await db.query.draftQueue.findMany({
|
||||
where: and(
|
||||
eq(schema.draftQueue.seasonId, seasonId),
|
||||
eq(schema.draftQueue.teamId, teamId)
|
||||
),
|
||||
orderBy: asc(schema.draftQueue.queuePosition),
|
||||
});
|
||||
|
||||
let participantId: string | null = null;
|
||||
|
||||
// Try to pick from queue first
|
||||
if (queueItems.length > 0) {
|
||||
// Get already drafted participant IDs
|
||||
const draftedPicks = await db.query.draftPicks.findMany({
|
||||
where: eq(schema.draftPicks.seasonId, seasonId),
|
||||
});
|
||||
const draftedParticipantIds = draftedPicks.map((p: any) => p.participantId);
|
||||
|
||||
// Find first queued participant that hasn't been drafted
|
||||
const availableQueueItem = queueItems.find(
|
||||
(item: any) => !draftedParticipantIds.includes(item.participantId)
|
||||
);
|
||||
|
||||
if (availableQueueItem) {
|
||||
participantId = availableQueueItem.participantId;
|
||||
console.log(`[Timer] Auto-picking from queue: ${participantId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// If no valid queue item, pick highest EV available participant
|
||||
if (!participantId) {
|
||||
// Get sports season IDs for this season
|
||||
const seasonSports = await db.query.seasonTemplateSports.findMany({
|
||||
where: eq(schema.seasonTemplateSports.templateId, season.templateId!),
|
||||
});
|
||||
const sportsSeasonIds = seasonSports.map((ss: any) => ss.sportsSeasonId);
|
||||
|
||||
if (sportsSeasonIds.length === 0) {
|
||||
console.error(`[Timer] No sports seasons found for season ${seasonId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get already drafted participant IDs
|
||||
const draftedPicks = await db.query.draftPicks.findMany({
|
||||
where: eq(schema.draftPicks.seasonId, seasonId),
|
||||
});
|
||||
const draftedParticipantIds = draftedPicks.map((p: any) => p.participantId);
|
||||
|
||||
// Query available participants from the sports seasons
|
||||
const availableParticipants = await db.query.participants.findMany({
|
||||
where: and(
|
||||
sportsSeasonIds.length === 1
|
||||
? eq(schema.participants.sportsSeasonId, sportsSeasonIds[0])
|
||||
: inArray(schema.participants.sportsSeasonId, sportsSeasonIds),
|
||||
draftedParticipantIds.length > 0
|
||||
? notInArray(schema.participants.id, draftedParticipantIds)
|
||||
: undefined
|
||||
),
|
||||
orderBy: desc(schema.participants.expectedValue),
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
if (availableParticipants.length === 0) {
|
||||
console.error(`[Timer] No available participants for auto-pick in season ${seasonId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
participantId = availableParticipants[0].id;
|
||||
console.log(`[Timer] Auto-picking highest EV: ${participantId}`);
|
||||
}
|
||||
|
||||
// Calculate round and pick in round
|
||||
const draftSlots = await db.query.draftSlots.findMany({
|
||||
where: eq(schema.draftSlots.seasonId, seasonId),
|
||||
orderBy: asc(schema.draftSlots.draftOrder),
|
||||
});
|
||||
const totalTeams = draftSlots.length;
|
||||
const round = Math.ceil(pickNumber / totalTeams);
|
||||
const pickInRound = ((pickNumber - 1) % totalTeams) + 1;
|
||||
|
||||
// Create the draft pick
|
||||
const [createdPick] = await db.insert(schema.draftPicks).values({
|
||||
// Execute the autopick using the unified function
|
||||
const result = await executeAutoPick({
|
||||
seasonId,
|
||||
teamId,
|
||||
participantId,
|
||||
pickNumber,
|
||||
round,
|
||||
pickInRound,
|
||||
pickedByUserId: "", // Auto-pick has no user
|
||||
pickedByType: "auto",
|
||||
timeUsed: season.draftInitialTime || 120, // Used all time
|
||||
createdAt: new Date(),
|
||||
}).returning();
|
||||
|
||||
console.log(`[Timer] Auto-pick created: Pick ${pickNumber} - Participant ${participantId}`);
|
||||
|
||||
// Remove from ALL team queues in this season (participant is now drafted)
|
||||
await db
|
||||
.delete(schema.draftQueue)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.draftQueue.seasonId, seasonId),
|
||||
eq(schema.draftQueue.participantId, participantId)
|
||||
)
|
||||
);
|
||||
|
||||
// Check if draft is complete
|
||||
const totalPicks = totalTeams * (season.draftRounds || 1);
|
||||
const allPicks = await db.query.draftPicks.findMany({
|
||||
where: eq(schema.draftPicks.seasonId, seasonId),
|
||||
});
|
||||
const isDraftComplete = allPicks.length >= totalPicks;
|
||||
const nextPickNumber = isDraftComplete ? pickNumber : pickNumber + 1;
|
||||
|
||||
// If autodraft was enabled with "next_pick" mode, disable it now
|
||||
if (autodraftSettings?.isEnabled && autodraftSettings.mode === "next_pick") {
|
||||
console.log(`[Timer] Disabling autodraft for team ${teamId} after next_pick`);
|
||||
await db
|
||||
.update(schema.autodraftSettings)
|
||||
.set({
|
||||
isEnabled: false,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(schema.autodraftSettings.id, autodraftSettings.id));
|
||||
|
||||
// Emit socket event to notify clients
|
||||
io.to(`draft-${seasonId}`).emit("autodraft-updated", {
|
||||
teamId,
|
||||
isEnabled: false,
|
||||
mode: autodraftSettings.mode,
|
||||
});
|
||||
}
|
||||
|
||||
// Add increment to the team that just picked
|
||||
const pickingTeamTimer = await db.query.draftTimers.findFirst({
|
||||
where: and(
|
||||
eq(schema.draftTimers.seasonId, seasonId),
|
||||
eq(schema.draftTimers.teamId, teamId)
|
||||
),
|
||||
triggeredBy: "timer",
|
||||
autodraftSettings,
|
||||
db,
|
||||
});
|
||||
|
||||
if (pickingTeamTimer) {
|
||||
const incrementTime = season.draftIncrementTime || 30;
|
||||
const newTimeRemaining = pickingTeamTimer.timeRemaining + incrementTime;
|
||||
await db
|
||||
.update(schema.draftTimers)
|
||||
.set({
|
||||
timeRemaining: newTimeRemaining,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(schema.draftTimers.id, pickingTeamTimer.id));
|
||||
|
||||
// Emit timer update to all clients
|
||||
io.to(`draft-${seasonId}`).emit("timer-update", {
|
||||
seasonId,
|
||||
teamId,
|
||||
timeRemaining: newTimeRemaining,
|
||||
currentPickNumber: pickNumber,
|
||||
});
|
||||
}
|
||||
|
||||
if (isDraftComplete) {
|
||||
// Draft complete
|
||||
await db
|
||||
.update(schema.seasons)
|
||||
.set({ status: "active" })
|
||||
.where(eq(schema.seasons.id, seasonId));
|
||||
|
||||
io.to(`draft-${seasonId}`).emit("draft-completed");
|
||||
console.log(`[Timer] Draft completed for season ${seasonId}`);
|
||||
} else {
|
||||
// Move to next pick
|
||||
await db
|
||||
.update(schema.seasons)
|
||||
.set({ currentPickNumber: nextPickNumber })
|
||||
.where(eq(schema.seasons.id, seasonId));
|
||||
}
|
||||
|
||||
// Fetch the complete pick with all relations (same as API routes do)
|
||||
const completePick = await db
|
||||
.select({
|
||||
id: schema.draftPicks.id,
|
||||
seasonId: schema.draftPicks.seasonId,
|
||||
teamId: schema.draftPicks.teamId,
|
||||
participantId: schema.draftPicks.participantId,
|
||||
pickNumber: schema.draftPicks.pickNumber,
|
||||
round: schema.draftPicks.round,
|
||||
pickInRound: schema.draftPicks.pickInRound,
|
||||
pickedByUserId: schema.draftPicks.pickedByUserId,
|
||||
pickedByType: schema.draftPicks.pickedByType,
|
||||
timeUsed: schema.draftPicks.timeUsed,
|
||||
createdAt: schema.draftPicks.createdAt,
|
||||
team: schema.teams,
|
||||
participant: schema.participants,
|
||||
sport: schema.sports,
|
||||
})
|
||||
.from(schema.draftPicks)
|
||||
.innerJoin(schema.teams, eq(schema.draftPicks.teamId, schema.teams.id))
|
||||
.innerJoin(schema.participants, eq(schema.draftPicks.participantId, schema.participants.id))
|
||||
.innerJoin(schema.sportsSeasons, eq(schema.participants.sportsSeasonId, schema.sportsSeasons.id))
|
||||
.innerJoin(schema.sports, eq(schema.sportsSeasons.sportId, schema.sports.id))
|
||||
.where(eq(schema.draftPicks.id, createdPick.id))
|
||||
.limit(1);
|
||||
|
||||
if (completePick.length === 0) {
|
||||
console.error(`[Timer] Could not fetch complete pick data`);
|
||||
if (!result.success) {
|
||||
console.error(`[Timer] Auto-pick failed: ${result.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Emit pick-made event with the same structure as API routes
|
||||
io.to(`draft-${seasonId}`).emit("pick-made", {
|
||||
pick: {
|
||||
...completePick[0],
|
||||
participant: {
|
||||
...completePick[0].participant,
|
||||
sport: completePick[0].sport,
|
||||
},
|
||||
},
|
||||
nextPickNumber,
|
||||
isDraftComplete,
|
||||
});
|
||||
|
||||
console.log(`[Timer] Auto-pick complete and broadcasted`);
|
||||
} catch (error) {
|
||||
console.error("[Timer] Error in triggerAutoPick:", error);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
"server/**/*.ts",
|
||||
"database/**/*.ts",
|
||||
"app/contexts/**/*.ts",
|
||||
"app/models/**/*.ts",
|
||||
"app/lib/**/*.ts",
|
||||
"vite.config.ts"
|
||||
],
|
||||
"exclude": [
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@
|
|||
"server.ts",
|
||||
"server/**/*.ts",
|
||||
"database/**/*.ts",
|
||||
"app/database/**/*.ts"
|
||||
"app/database/**/*.ts",
|
||||
"app/models/**/*.ts",
|
||||
"app/lib/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue