feat: Implement autodraft chain logic and timer initialization for next team picks
This commit is contained in:
parent
1825c0e271
commit
918e9ff04a
3 changed files with 314 additions and 27 deletions
|
|
@ -7,6 +7,64 @@ import { getParticipantsForSeasonWithSports } from "./participant";
|
||||||
import { getSeasonSportsSimple } from "./season-sport";
|
import { getSeasonSportsSimple } from "./season-sport";
|
||||||
import { calculateDraftEligibility } from "~/lib/draft-eligibility";
|
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
|
* Auto-pick for a team when their timer runs out
|
||||||
* 1. Check queue - pick first eligible item if available (cleans up ineligible items)
|
* 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}`
|
`[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 nextPickNumber = pickNumber + 1;
|
||||||
const totalPicks = totalTeams * season.draftRounds;
|
const totalPicks = totalTeams * season.draftRounds;
|
||||||
const isDraftComplete = nextPickNumber > totalPicks;
|
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
|
// Add increment to the team that just picked
|
||||||
const currentTimer = await db.query.draftTimers.findFirst({
|
const currentTimer = await db.query.draftTimers.findFirst({
|
||||||
where: and(
|
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)
|
// Remove from ALL team queues in this season (participant is now drafted)
|
||||||
await db
|
await db
|
||||||
.delete(schema.draftQueue)
|
.delete(schema.draftQueue)
|
||||||
|
|
@ -568,6 +692,17 @@ export async function executeAutoPick(params: {
|
||||||
console.error("[AutoPick] Socket.IO events error:", error);
|
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 {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
pick: draftPick,
|
pick: draftPick,
|
||||||
|
|
|
||||||
|
|
@ -130,19 +130,11 @@ export async function action(args: any) {
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
// Update season's current pick number
|
// Calculate next pick info (before updating season)
|
||||||
const nextPickNumber = pickNumber + 1;
|
const nextPickNumber = pickNumber + 1;
|
||||||
const totalPicks = totalTeams * season.draftRounds;
|
const totalPicks = totalTeams * season.draftRounds;
|
||||||
const isDraftComplete = nextPickNumber > totalPicks;
|
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
|
// Add increment to the team that just picked
|
||||||
const currentTimer = await db.query.draftTimers.findFirst({
|
const currentTimer = await db.query.draftTimers.findFirst({
|
||||||
where: and(
|
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)
|
// Remove from ALL team queues in this season (participant is now drafted)
|
||||||
await db
|
await db
|
||||||
.delete(schema.draftQueue)
|
.delete(schema.draftQueue)
|
||||||
|
|
@ -222,6 +286,18 @@ export async function action(args: any) {
|
||||||
console.error("Socket.IO error:", error);
|
console.error("Socket.IO error:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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({
|
return Response.json({
|
||||||
success: true,
|
success: true,
|
||||||
pick: draftPick,
|
pick: draftPick,
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,11 @@ export async function action(args: any) {
|
||||||
console.error("Socket.IO participant-removed-from-queues error:", error);
|
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
|
// Add increment to the team that just picked
|
||||||
const currentTimer = await db.query.draftTimers.findFirst({
|
const currentTimer = await db.query.draftTimers.findFirst({
|
||||||
where: and(
|
where: and(
|
||||||
|
|
@ -194,11 +199,70 @@ export async function action(args: any) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update season's current pick number
|
// Initialize timer for the next team BEFORE updating pick number (prevents race condition)
|
||||||
const nextPickNumber = currentPickNumber + 1;
|
if (!isDraftComplete) {
|
||||||
const totalPicks = totalTeams * season.draftRounds;
|
// Calculate which team is next using snake draft logic
|
||||||
const isDraftComplete = nextPickNumber > totalPicks;
|
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
|
await db
|
||||||
.update(schema.seasons)
|
.update(schema.seasons)
|
||||||
.set({
|
.set({
|
||||||
|
|
@ -231,6 +295,18 @@ export async function action(args: any) {
|
||||||
console.error("Socket.IO error:", error);
|
console.error("Socket.IO error:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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({
|
return Response.json({
|
||||||
success: true,
|
success: true,
|
||||||
pick: draftPick,
|
pick: draftPick,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue