diff --git a/app/models/cs2-major-stage.ts b/app/models/cs2-major-stage.ts index a04bb38..57d34b4 100644 --- a/app/models/cs2-major-stage.ts +++ b/app/models/cs2-major-stage.ts @@ -153,7 +153,7 @@ export async function markCs2StageEliminations( db .update(cs2MajorStageResults) .set({ - stageEliminated: sql`stage_entry`, // eliminated at their current stage + stageEliminated: sql`${cs2MajorStageResults.stageEntry}`, // eliminated at their current stage stageEliminatedWins: winsAtElimination, updatedAt: now, }) diff --git a/app/routes/admin.sports-seasons.$id.events.$eventId.cs2-setup.tsx b/app/routes/admin.sports-seasons.$id.events.$eventId.cs2-setup.tsx index a9ba635..c438ef8 100644 --- a/app/routes/admin.sports-seasons.$id.events.$eventId.cs2-setup.tsx +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.cs2-setup.tsx @@ -120,9 +120,7 @@ export default function AdminCs2Setup() { const isSubmitting = navigation.state === 'submitting'; // Build maps from existing stage results - const stageMap = new Map(stageResults.map(r => [r.participantId, r])); const assignedIds = new Set(stageResults.map(r => r.participantId)); - const unassignedParticipants = participants.filter(p => !assignedIds.has(p.id)); // Stage assignment state (local, submitted via form) const [stageSelections, setStageSelections] = useState>(() => { @@ -131,6 +129,13 @@ export default function AdminCs2Setup() { return initial; }); + // Elimination checkbox state — tracks which teams are checked as eliminated + const [eliminatedChecked, setEliminatedChecked] = useState>(() => { + const initial: Record = {}; + stageResults.forEach(r => { initial[r.participantId] = r.stageEliminated !== null; }); + return initial; + }); + // Group participants by stage for display const byStage: Record = { 1: [], 2: [], 3: [] }; for (const r of stageResults) { @@ -257,12 +262,12 @@ export default function AdminCs2Setup() {
{teamsInStage.map(r => { - const isEliminated = r.stageEliminated !== null; + const isEliminated = eliminatedChecked[r.participantId] ?? false; return (
{r.participantName} - {isEliminated && ( + {r.stageEliminated !== null && ( eliminated {r.stageEliminatedWins}-3 @@ -271,23 +276,20 @@ export default function AdminCs2Setup() { -
+ {isEliminated && ( -
+ )} + {!isEliminated &&
}
); })} diff --git a/app/routes/admin.sports-seasons.$id.events.$eventId.server.ts b/app/routes/admin.sports-seasons.$id.events.$eventId.server.ts index 9bc792a..8dfc594 100644 --- a/app/routes/admin.sports-seasons.$id.events.$eventId.server.ts +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.server.ts @@ -63,7 +63,7 @@ export async function loader({ params }: Route.LoaderArgs) { return { sportsSeason: sportsSeason as typeof sportsSeason & { - sport: { id: string; name: string; type: string; slug: string }; + sport: { id: string; name: string; type: string; slug: string; simulatorType: string | null }; }, event, participants, diff --git a/app/routes/admin.sports-seasons.$id.events.$eventId.tsx b/app/routes/admin.sports-seasons.$id.events.$eventId.tsx index ac4bda0..8e8513a 100644 --- a/app/routes/admin.sports-seasons.$id.events.$eventId.tsx +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.tsx @@ -230,6 +230,14 @@ export default function EventResults({ )} + {sportsSeason.sport?.simulatorType === "cs2_major_qualifying_points" && ( + + )} {event.isComplete ? ( diff --git a/app/services/simulations/cs-major-simulator.ts b/app/services/simulations/cs-major-simulator.ts index 31859a8..f9d72e8 100644 --- a/app/services/simulations/cs-major-simulator.ts +++ b/app/services/simulations/cs-major-simulator.ts @@ -527,7 +527,11 @@ export class CSMajorSimulator implements Simulator { /** * Simulate one CS2 Major and return QP earned per participant. - * Uses explicit stage assignments if provided; otherwise infers from world ranking. + * + * If stage results are provided, uses them to determine field composition + * and to lock in results for any completed stages, only simulating the + * remaining stages. A stage is considered complete when the expected 8 + * eliminations for that stage have been recorded. * * Returns a Map from participantId → QP earned in this major. */ @@ -537,74 +541,118 @@ function simulateOneMajor( qpConfig: Map ): Map { const qpMap = new Map(); + const poolById = new Map(pool.map((t) => [t.id, t])); - // Determine the 32-team field and stage assignments. + // ── Determine field and stage assignments ───────────────────────────────── let stage1Teams: TeamWithElo[]; let stage2Direct: TeamWithElo[]; let stage3Direct: TeamWithElo[]; if (stageResults && stageResults.size > 0) { - // Use explicit stage assignments from the database. const s1: TeamWithElo[] = []; const s2: TeamWithElo[] = []; const s3: TeamWithElo[] = []; - for (const [participantId, result] of stageResults) { - const team = pool.find((t) => t.id === participantId); + const team = poolById.get(participantId); if (!team) continue; - - // If result is already complete (stageEliminated is set), we can skip simulation - // for that team and use the known outcome. For simplicity in this iteration, - // we still simulate incomplete stages using Elo. if (result.stageEntry === 1) s1.push(team); else if (result.stageEntry === 2) s2.push(team); else if (result.stageEntry === 3) s3.push(team); } - stage1Teams = s1; stage2Direct = s2; stage3Direct = s3; } else { - // Infer stage assignments from world ranking. const field = sampleField(pool, FIELD_SIZE); const sorted = [...field].sort((a, b) => a.rank - b.rank); - - // Top 8 → Stage 3 (Legends), next 8 → Stage 2 (Challengers), bottom 16 → Stage 1 stage3Direct = sorted.slice(0, 8); stage2Direct = sorted.slice(8, 16); stage1Teams = sorted.slice(16, 32); } - // Stage 1: Swiss Bo1, 16 → 8 advance - const stage1Result = simulateSwiss(stage1Teams, false); + // ── Lock in known stage results ─────────────────────────────────────────── + // A stage is "complete" when exactly 8 teams have been recorded as + // eliminated at that stage (the expected output of each Swiss stage). + const eliminatedAtStage = (stageNum: number) => + stageResults + ? [...stageResults.entries()] + .filter(([, r]) => r.stageEliminated === stageNum) + .map(([id, r]) => ({ + id, + elo: poolById.get(id)?.elo ?? FALLBACK_ELO, + rank: poolById.get(id)?.rank ?? 9999, + wins: r.stageEliminatedWins ?? 0, + })) + : []; - // Stage 2: Swiss Bo1/Bo3, 16 → 8 advance - // Use Bo1 for Stage 2 (matches are Bo1 early rounds, Bo3 for deciders; - // we simplify to Bo1 for Stage 2 and Bo3 for Stage 3) - const stage2Teams = [...stage2Direct, ...stage1Result.advanced]; - const stage2Result = simulateSwiss(stage2Teams, false); + const stage1Elim = eliminatedAtStage(1); + const stage2Elim = eliminatedAtStage(2); + const stage3Elim = eliminatedAtStage(3); - // Stage 3: Swiss all Bo3, 16 → 8 advance - const stage3Teams = [...stage3Direct, ...stage2Result.advanced]; - const stage3Result = simulateSwiss(stage3Teams, true); + const stage1Complete = stage1Elim.length === 8; + const stage2Complete = stage2Elim.length === 8; + const stage3Complete = stage3Elim.length === 8; - // Champions Stage: 8-team single elimination - const champResult = simulateChampionsStage(stage3Result.advanced); + // ── Stage 1 ─────────────────────────────────────────────────────────────── + let stage1Advanced: TeamWithElo[]; + let stage1EliminatedFinal: Array<{ id: string; elo: number; rank: number; wins: number }>; - // Assign QP from Champions Stage (placements 1–8) + if (stage1Complete) { + // Use known results: teams that entered Stage 1 but weren't eliminated there advanced + const stage1EliminatedIds = new Set(stage1Elim.map((t) => t.id)); + stage1Advanced = stage1Teams.filter((t) => !stage1EliminatedIds.has(t.id)); + stage1EliminatedFinal = stage1Elim; + } else { + const result = simulateSwiss(stage1Teams, false); + stage1Advanced = result.advanced; + stage1EliminatedFinal = result.eliminated; + } + + // ── Stage 2 ─────────────────────────────────────────────────────────────── + const stage2Teams = [...stage2Direct, ...stage1Advanced]; + let stage2Advanced: TeamWithElo[]; + let stage2EliminatedFinal: Array<{ id: string; elo: number; rank: number; wins: number }>; + + if (stage2Complete) { + const stage2EliminatedIds = new Set(stage2Elim.map((t) => t.id)); + stage2Advanced = stage2Teams.filter((t) => !stage2EliminatedIds.has(t.id)); + stage2EliminatedFinal = stage2Elim; + } else { + const result = simulateSwiss(stage2Teams, false); + stage2Advanced = result.advanced; + stage2EliminatedFinal = result.eliminated; + } + + // ── Stage 3 ─────────────────────────────────────────────────────────────── + const stage3Teams = [...stage3Direct, ...stage2Advanced]; + let champTeams: TeamWithElo[]; + let stage3EliminatedFinal: Array<{ id: string; elo: number; rank: number; wins: number }>; + + if (stage3Complete) { + const stage3EliminatedIds = new Set(stage3Elim.map((t) => t.id)); + champTeams = stage3Teams.filter((t) => !stage3EliminatedIds.has(t.id)); + stage3EliminatedFinal = stage3Elim; + } else { + const result = simulateSwiss(stage3Teams, true); + champTeams = result.advanced; + stage3EliminatedFinal = result.eliminated; + } + + // ── Champions Stage ─────────────────────────────────────────────────────── + const champResult = simulateChampionsStage(champTeams); + + // ── Assign QP ───────────────────────────────────────────────────────────── for (const [pid, placement] of champResult.placements) { qpMap.set(pid, qpConfig.get(placement) ?? 0); } - // Assign QP for Stage 3 exits (placements 9–16), sub-ranked by wins - const stage3ExitQP = calcStage3ExitQP(stage3Result.eliminated, qpConfig); + const stage3ExitQP = calcStage3ExitQP(stage3EliminatedFinal, qpConfig); for (const [pid, qp] of stage3ExitQP) { qpMap.set(pid, qp); } - // Stage 1 and 2 exits get 0 QP - for (const t of stage1Result.eliminated) qpMap.set(t.id, 0); - for (const t of stage2Result.eliminated) qpMap.set(t.id, 0); + for (const t of stage1EliminatedFinal) qpMap.set(t.id, 0); + for (const t of stage2EliminatedFinal) qpMap.set(t.id, 0); return qpMap; }