feat: Implement autodraft chain logic and timer initialization for next team picks

This commit is contained in:
Chris Parsons 2025-10-25 22:11:10 -07:00
parent 1825c0e271
commit 918e9ff04a
3 changed files with 314 additions and 27 deletions

View file

@ -7,6 +7,64 @@ import { getParticipantsForSeasonWithSports } from "./participant";
import { getSeasonSportsSimple } from "./season-sport";
import { calculateDraftEligibility } from "~/lib/draft-eligibility";
/**
* Check if the next team has autodraft enabled and immediately execute their pick
* This is called after a pick is made to chain autodraft picks
*/
export async function checkAndTriggerNextAutodraft(params: {
seasonId: string;
nextPickNumber: number;
totalTeams: number;
draftSlots: any[];
db?: ReturnType<typeof database>;
}): Promise<void> {
const { seasonId, nextPickNumber, totalTeams, draftSlots, db: providedDb } = params;
const db = providedDb || database();
// Calculate which team is next using snake draft logic
const nextRound = Math.ceil(nextPickNumber / totalTeams);
const isNextRoundEven = nextRound % 2 === 0;
let nextPickInRound = ((nextPickNumber - 1) % totalTeams) + 1;
// Apply snake draft reversal for even rounds
if (isNextRoundEven) {
nextPickInRound = totalTeams - nextPickInRound + 1;
}
const nextDraftSlot = draftSlots.find((slot) => slot.draftOrder === nextPickInRound);
if (!nextDraftSlot) return;
const nextTeamId = nextDraftSlot.teamId;
// Check if next team has autodraft enabled
const autodraftSettings = await db.query.autodraftSettings.findFirst({
where: and(
eq(schema.autodraftSettings.seasonId, seasonId),
eq(schema.autodraftSettings.teamId, nextTeamId)
),
});
if (autodraftSettings?.isEnabled) {
console.log(
`[AutodraftChain] Team ${nextTeamId} has autodraft enabled, triggering immediate pick for pick ${nextPickNumber}`
);
// Immediately execute autopick for this team
await executeAutoPick({
seasonId,
teamId: nextTeamId,
pickNumber: nextPickNumber,
triggeredBy: "timer", // Use "timer" to indicate automatic (not commissioner-forced)
autodraftSettings,
db,
});
} else {
console.log(
`[AutodraftChain] Team ${nextTeamId} does not have autodraft enabled, waiting for manual pick`
);
}
}
/**
* Auto-pick for a team when their timer runs out
* 1. Check queue - pick first eligible item if available (cleans up ineligible items)
@ -449,19 +507,11 @@ export async function executeAutoPick(params: {
`[AutoPick] Pick created - ${triggeredBy} triggered - Pick ${pickNumber} - Participant ${participantId}`
);
// Update season's current pick number and check if draft complete
// Calculate next pick info (before updating season)
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(
@ -497,6 +547,80 @@ export async function executeAutoPick(params: {
}
}
// Initialize timer for the next team BEFORE updating pick number (prevents race condition)
if (!isDraftComplete) {
// Calculate which team is next using snake draft logic
const nextRound = Math.ceil(nextPickNumber / totalTeams);
const isNextRoundEven = nextRound % 2 === 0;
let nextPickInRound = ((nextPickNumber - 1) % totalTeams) + 1;
// Apply snake draft reversal for even rounds
if (isNextRoundEven) {
nextPickInRound = totalTeams - nextPickInRound + 1;
}
const nextDraftSlot = draftSlots.find((slot) => slot.draftOrder === nextPickInRound);
if (nextDraftSlot) {
const nextTeamId = nextDraftSlot.teamId;
const initialTime = season.draftInitialTime || 120;
console.log(
`[AutoPick] Initializing timer for next team ${nextTeamId} with ${initialTime}s (pick ${nextPickNumber})`
);
// Check if timer already exists for next team
const nextTimer = await db.query.draftTimers.findFirst({
where: and(
eq(schema.draftTimers.seasonId, seasonId),
eq(schema.draftTimers.teamId, nextTeamId)
),
});
if (nextTimer) {
// Update existing timer
await db
.update(schema.draftTimers)
.set({
timeRemaining: initialTime,
updatedAt: new Date(),
})
.where(eq(schema.draftTimers.id, nextTimer.id));
} else {
// Create new timer if it doesn't exist
await db.insert(schema.draftTimers).values({
seasonId,
teamId: nextTeamId,
timeRemaining: initialTime,
});
}
// Emit timer update for next team
try {
const io = (global as any).__socketIO;
if (io) {
io.to(`draft-${seasonId}`).emit("timer-update", {
seasonId,
teamId: nextTeamId,
timeRemaining: initialTime,
currentPickNumber: nextPickNumber,
});
}
} catch (error) {
console.error("[AutoPick] Socket.IO next timer-update error:", error);
}
}
}
// Update season's current pick number (AFTER initializing next timer to prevent race condition)
await db
.update(schema.seasons)
.set({
currentPickNumber: isDraftComplete ? pickNumber : nextPickNumber,
status: isDraftComplete ? "active" : season.status,
})
.where(eq(schema.seasons.id, seasonId));
// Remove from ALL team queues in this season (participant is now drafted)
await db
.delete(schema.draftQueue)
@ -568,6 +692,17 @@ export async function executeAutoPick(params: {
console.error("[AutoPick] Socket.IO events error:", error);
}
// Check if next team has autodraft enabled and trigger immediately
if (!isDraftComplete) {
await checkAndTriggerNextAutodraft({
seasonId,
nextPickNumber,
totalTeams,
draftSlots,
db,
});
}
return {
success: true,
pick: draftPick,

View file

@ -130,19 +130,11 @@ export async function action(args: any) {
})
.returning();
// Update season's current pick number
// Calculate next pick info (before updating season)
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(
@ -176,6 +168,78 @@ export async function action(args: any) {
}
}
// Initialize timer for the next team BEFORE updating pick number (prevents race condition)
if (!isDraftComplete) {
// Calculate which team is next using snake draft logic
const nextRound = Math.ceil(nextPickNumber / totalTeams);
const isNextRoundEven = nextRound % 2 === 0;
let nextPickInRound = ((nextPickNumber - 1) % totalTeams) + 1;
// Apply snake draft reversal for even rounds
if (isNextRoundEven) {
nextPickInRound = totalTeams - nextPickInRound + 1;
}
const nextDraftSlot = draftSlots.find((slot) => slot.draftOrder === nextPickInRound);
if (nextDraftSlot) {
const nextTeamId = nextDraftSlot.teamId;
const initialTime = season.draftInitialTime || 120;
console.log(
`[ForceManualPick] Initializing timer for next team ${nextTeamId} with ${initialTime}s (pick ${nextPickNumber})`
);
// Check if timer already exists for next team
const nextTimer = await db.query.draftTimers.findFirst({
where: and(
eq(schema.draftTimers.seasonId, seasonId),
eq(schema.draftTimers.teamId, nextTeamId)
),
});
if (nextTimer) {
// Update existing timer
await db
.update(schema.draftTimers)
.set({
timeRemaining: initialTime,
updatedAt: new Date(),
})
.where(eq(schema.draftTimers.id, nextTimer.id));
} else {
// Create new timer if it doesn't exist
await db.insert(schema.draftTimers).values({
seasonId,
teamId: nextTeamId,
timeRemaining: initialTime,
});
}
// Emit timer update for next team
try {
const io = (global as any).__socketIO;
io.to(`draft-${seasonId}`).emit("timer-update", {
seasonId,
teamId: nextTeamId,
timeRemaining: initialTime,
currentPickNumber: nextPickNumber,
});
} catch (error) {
console.error("Socket.IO next timer-update error:", error);
}
}
}
// Update season's current pick number (AFTER initializing next timer to prevent race condition)
await db
.update(schema.seasons)
.set({
currentPickNumber: isDraftComplete ? pickNumber : nextPickNumber,
status: isDraftComplete ? "active" : season.status,
})
.where(eq(schema.seasons.id, seasonId));
// Remove from ALL team queues in this season (participant is now drafted)
await db
.delete(schema.draftQueue)
@ -200,7 +264,7 @@ export async function action(args: any) {
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,
@ -222,8 +286,20 @@ export async function action(args: any) {
console.error("Socket.IO error:", error);
}
return Response.json({
success: true,
// Check if next team has autodraft enabled and trigger immediately
if (!isDraftComplete) {
const { checkAndTriggerNextAutodraft } = await import("~/models/draft-utils");
await checkAndTriggerNextAutodraft({
seasonId,
nextPickNumber,
totalTeams,
draftSlots,
db,
});
}
return Response.json({
success: true,
pick: draftPick,
nextPickNumber: isDraftComplete ? pickNumber : nextPickNumber,
isDraftComplete,

View file

@ -161,6 +161,11 @@ export async function action(args: any) {
console.error("Socket.IO participant-removed-from-queues error:", error);
}
// Calculate next pick info (before updating season)
const nextPickNumber = currentPickNumber + 1;
const totalPicks = totalTeams * season.draftRounds;
const isDraftComplete = nextPickNumber > totalPicks;
// Add increment to the team that just picked
const currentTimer = await db.query.draftTimers.findFirst({
where: and(
@ -194,11 +199,70 @@ export async function action(args: any) {
}
}
// Update season's current pick number
const nextPickNumber = currentPickNumber + 1;
const totalPicks = totalTeams * season.draftRounds;
const isDraftComplete = nextPickNumber > totalPicks;
// Initialize timer for the next team BEFORE updating pick number (prevents race condition)
if (!isDraftComplete) {
// Calculate which team is next using snake draft logic
const nextRound = Math.ceil(nextPickNumber / totalTeams);
const isNextRoundEven = nextRound % 2 === 0;
let nextPickInRound = ((nextPickNumber - 1) % totalTeams) + 1;
// Apply snake draft reversal for even rounds
if (isSnakeDraft && isNextRoundEven) {
nextPickInRound = totalTeams - nextPickInRound + 1;
}
const nextDraftSlot = draftSlots.find((slot) => slot.draftOrder === nextPickInRound);
if (nextDraftSlot) {
const nextTeamId = nextDraftSlot.teamId;
const initialTime = season.draftInitialTime || 120;
console.log(
`[MakePick] Initializing timer for next team ${nextTeamId} with ${initialTime}s (pick ${nextPickNumber})`
);
// Check if timer already exists for next team
const nextTimer = await db.query.draftTimers.findFirst({
where: and(
eq(schema.draftTimers.seasonId, seasonId),
eq(schema.draftTimers.teamId, nextTeamId)
),
});
if (nextTimer) {
// Update existing timer
await db
.update(schema.draftTimers)
.set({
timeRemaining: initialTime,
updatedAt: new Date(),
})
.where(eq(schema.draftTimers.id, nextTimer.id));
} else {
// Create new timer if it doesn't exist
await db.insert(schema.draftTimers).values({
seasonId,
teamId: nextTeamId,
timeRemaining: initialTime,
});
}
// Emit timer update for next team
try {
const io = (global as any).__socketIO;
io.to(`draft-${seasonId}`).emit("timer-update", {
seasonId,
teamId: nextTeamId,
timeRemaining: initialTime,
currentPickNumber: nextPickNumber,
});
} catch (error) {
console.error("Socket.IO next timer-update error:", error);
}
}
}
// Update season's current pick number (AFTER initializing next timer to prevent race condition)
await db
.update(schema.seasons)
.set({
@ -231,8 +295,20 @@ export async function action(args: any) {
console.error("Socket.IO error:", error);
}
return Response.json({
success: true,
// Check if next team has autodraft enabled and trigger immediately
if (!isDraftComplete) {
const { checkAndTriggerNextAutodraft } = await import("~/models/draft-utils");
await checkAndTriggerNextAutodraft({
seasonId,
nextPickNumber,
totalTeams,
draftSlots,
db,
});
}
return Response.json({
success: true,
pick: draftPick,
nextPickNumber: isDraftComplete ? currentPickNumber : nextPickNumber,
isDraftComplete,