Fix all remaining code review issues

- cs2-major-stage.ts: use schema column reference for stageEliminated
  in markCs2StageEliminations instead of raw SQL string
- cs-major-simulator.ts: simulateOneMajor now locks in known stage
  results when a stage is complete (8 recorded eliminations), only
  simulating the remaining stages during live events
- admin event page: add CS2 Stage Setup button for cs2_major_qualifying_points
  simulator types; expose simulatorType in server loader type cast
- cs2-setup.tsx: replace document.getElementById DOM manipulation with
  React state (eliminatedChecked map) for checkbox show/hide logic;
  remove unused stageMap and unassignedParticipants variables

https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
This commit is contained in:
Claude 2026-04-05 20:11:47 +00:00
parent a516dedb76
commit a0deb376a1
No known key found for this signature in database
5 changed files with 108 additions and 49 deletions

View file

@ -153,7 +153,7 @@ export async function markCs2StageEliminations(
db db
.update(cs2MajorStageResults) .update(cs2MajorStageResults)
.set({ .set({
stageEliminated: sql`stage_entry`, // eliminated at their current stage stageEliminated: sql`${cs2MajorStageResults.stageEntry}`, // eliminated at their current stage
stageEliminatedWins: winsAtElimination, stageEliminatedWins: winsAtElimination,
updatedAt: now, updatedAt: now,
}) })

View file

@ -120,9 +120,7 @@ export default function AdminCs2Setup() {
const isSubmitting = navigation.state === 'submitting'; const isSubmitting = navigation.state === 'submitting';
// Build maps from existing stage results // 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 assignedIds = new Set(stageResults.map(r => r.participantId));
const unassignedParticipants = participants.filter(p => !assignedIds.has(p.id));
// Stage assignment state (local, submitted via form) // Stage assignment state (local, submitted via form)
const [stageSelections, setStageSelections] = useState<Record<string, string>>(() => { const [stageSelections, setStageSelections] = useState<Record<string, string>>(() => {
@ -131,6 +129,13 @@ export default function AdminCs2Setup() {
return initial; return initial;
}); });
// Elimination checkbox state — tracks which teams are checked as eliminated
const [eliminatedChecked, setEliminatedChecked] = useState<Record<string, boolean>>(() => {
const initial: Record<string, boolean> = {};
stageResults.forEach(r => { initial[r.participantId] = r.stageEliminated !== null; });
return initial;
});
// Group participants by stage for display // Group participants by stage for display
const byStage: Record<number, typeof stageResults> = { 1: [], 2: [], 3: [] }; const byStage: Record<number, typeof stageResults> = { 1: [], 2: [], 3: [] };
for (const r of stageResults) { for (const r of stageResults) {
@ -257,12 +262,12 @@ export default function AdminCs2Setup() {
</h3> </h3>
<div className="space-y-1"> <div className="space-y-1">
{teamsInStage.map(r => { {teamsInStage.map(r => {
const isEliminated = r.stageEliminated !== null; const isEliminated = eliminatedChecked[r.participantId] ?? false;
return ( return (
<div key={r.participantId} className="grid grid-cols-[1fr_auto_200px] gap-x-3 items-center py-1 border-b border-border/40 last:border-0"> <div key={r.participantId} className="grid grid-cols-[1fr_auto_200px] gap-x-3 items-center py-1 border-b border-border/40 last:border-0">
<span className="text-sm flex items-center gap-2"> <span className="text-sm flex items-center gap-2">
{r.participantName} {r.participantName}
{isEliminated && ( {r.stageEliminated !== null && (
<Badge variant="outline" className="text-xs"> <Badge variant="outline" className="text-xs">
eliminated {r.stageEliminatedWins}-3 eliminated {r.stageEliminatedWins}-3
</Badge> </Badge>
@ -271,23 +276,20 @@ export default function AdminCs2Setup() {
<label className="flex items-center gap-2 text-sm cursor-pointer"> <label className="flex items-center gap-2 text-sm cursor-pointer">
<input <input
type="checkbox" type="checkbox"
defaultChecked={isEliminated} checked={isEliminated}
onChange={e => { onChange={e =>
// Show/hide the wins selector setEliminatedChecked(prev => ({
const winsEl = document.getElementById(`wins_${r.participantId}`); ...prev,
if (winsEl) winsEl.style.display = e.target.checked ? 'block' : 'none'; [r.participantId]: e.target.checked,
// Also show/hide the hidden input }))
const hiddenEl = document.getElementById(`elim_hidden_${r.participantId}`) as HTMLInputElement | null; }
if (hiddenEl) hiddenEl.disabled = !e.target.checked;
}}
className="rounded" className="rounded"
/> />
Eliminated Eliminated
</label> </label>
<div id={`wins_${r.participantId}`} style={{ display: isEliminated ? 'block' : 'none' }}> {isEliminated && (
<select <select
name={`elim_${r.participantId}`} name={`elim_${r.participantId}`}
id={`elim_hidden_${r.participantId}`}
defaultValue={r.stageEliminatedWins?.toString() ?? '0'} defaultValue={r.stageEliminatedWins?.toString() ?? '0'}
className="h-8 w-full text-sm rounded-md border border-input bg-background px-2" className="h-8 w-full text-sm rounded-md border border-input bg-background px-2"
> >
@ -295,7 +297,8 @@ export default function AdminCs2Setup() {
<option key={opt.value} value={opt.value}>{opt.label}</option> <option key={opt.value} value={opt.value}>{opt.label}</option>
))} ))}
</select> </select>
</div> )}
{!isEliminated && <div />}
</div> </div>
); );
})} })}

View file

@ -63,7 +63,7 @@ export async function loader({ params }: Route.LoaderArgs) {
return { return {
sportsSeason: sportsSeason as typeof sportsSeason & { 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, event,
participants, participants,

View file

@ -230,6 +230,14 @@ export default function EventResults({
</Link> </Link>
</Button> </Button>
)} )}
{sportsSeason.sport?.simulatorType === "cs2_major_qualifying_points" && (
<Button variant="outline" asChild>
<Link to={`/admin/sports-seasons/${sportsSeason.id}/events/${event.id}/cs2-setup`}>
<Brackets className="mr-2 h-4 w-4" />
CS2 Stage Setup
</Link>
</Button>
)}
{event.isComplete ? ( {event.isComplete ? (
<Badge variant="default" className="bg-emerald-500"> <Badge variant="default" className="bg-emerald-500">
<CheckCircle2 className="mr-1 h-3 w-3" /> <CheckCircle2 className="mr-1 h-3 w-3" />

View file

@ -527,7 +527,11 @@ export class CSMajorSimulator implements Simulator {
/** /**
* Simulate one CS2 Major and return QP earned per participant. * 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. * Returns a Map from participantId QP earned in this major.
*/ */
@ -537,74 +541,118 @@ function simulateOneMajor(
qpConfig: Map<number, number> qpConfig: Map<number, number>
): Map<string, number> { ): Map<string, number> {
const qpMap = new Map<string, number>(); const qpMap = new Map<string, number>();
const poolById = new Map<string, TeamWithElo>(pool.map((t) => [t.id, t]));
// Determine the 32-team field and stage assignments. // ── Determine field and stage assignments ─────────────────────────────────
let stage1Teams: TeamWithElo[]; let stage1Teams: TeamWithElo[];
let stage2Direct: TeamWithElo[]; let stage2Direct: TeamWithElo[];
let stage3Direct: TeamWithElo[]; let stage3Direct: TeamWithElo[];
if (stageResults && stageResults.size > 0) { if (stageResults && stageResults.size > 0) {
// Use explicit stage assignments from the database.
const s1: TeamWithElo[] = []; const s1: TeamWithElo[] = [];
const s2: TeamWithElo[] = []; const s2: TeamWithElo[] = [];
const s3: TeamWithElo[] = []; const s3: TeamWithElo[] = [];
for (const [participantId, result] of stageResults) { for (const [participantId, result] of stageResults) {
const team = pool.find((t) => t.id === participantId); const team = poolById.get(participantId);
if (!team) continue; 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); if (result.stageEntry === 1) s1.push(team);
else if (result.stageEntry === 2) s2.push(team); else if (result.stageEntry === 2) s2.push(team);
else if (result.stageEntry === 3) s3.push(team); else if (result.stageEntry === 3) s3.push(team);
} }
stage1Teams = s1; stage1Teams = s1;
stage2Direct = s2; stage2Direct = s2;
stage3Direct = s3; stage3Direct = s3;
} else { } else {
// Infer stage assignments from world ranking.
const field = sampleField(pool, FIELD_SIZE); const field = sampleField(pool, FIELD_SIZE);
const sorted = [...field].sort((a, b) => a.rank - b.rank); 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); stage3Direct = sorted.slice(0, 8);
stage2Direct = sorted.slice(8, 16); stage2Direct = sorted.slice(8, 16);
stage1Teams = sorted.slice(16, 32); stage1Teams = sorted.slice(16, 32);
} }
// Stage 1: Swiss Bo1, 16 → 8 advance // ── Lock in known stage results ───────────────────────────────────────────
const stage1Result = simulateSwiss(stage1Teams, false); // 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 const stage1Elim = eliminatedAtStage(1);
// Use Bo1 for Stage 2 (matches are Bo1 early rounds, Bo3 for deciders; const stage2Elim = eliminatedAtStage(2);
// we simplify to Bo1 for Stage 2 and Bo3 for Stage 3) const stage3Elim = eliminatedAtStage(3);
const stage2Teams = [...stage2Direct, ...stage1Result.advanced];
const stage2Result = simulateSwiss(stage2Teams, false);
// Stage 3: Swiss all Bo3, 16 → 8 advance const stage1Complete = stage1Elim.length === 8;
const stage3Teams = [...stage3Direct, ...stage2Result.advanced]; const stage2Complete = stage2Elim.length === 8;
const stage3Result = simulateSwiss(stage3Teams, true); const stage3Complete = stage3Elim.length === 8;
// Champions Stage: 8-team single elimination // ── Stage 1 ───────────────────────────────────────────────────────────────
const champResult = simulateChampionsStage(stage3Result.advanced); let stage1Advanced: TeamWithElo[];
let stage1EliminatedFinal: Array<{ id: string; elo: number; rank: number; wins: number }>;
// Assign QP from Champions Stage (placements 18) 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) { for (const [pid, placement] of champResult.placements) {
qpMap.set(pid, qpConfig.get(placement) ?? 0); qpMap.set(pid, qpConfig.get(placement) ?? 0);
} }
// Assign QP for Stage 3 exits (placements 916), sub-ranked by wins const stage3ExitQP = calcStage3ExitQP(stage3EliminatedFinal, qpConfig);
const stage3ExitQP = calcStage3ExitQP(stage3Result.eliminated, qpConfig);
for (const [pid, qp] of stage3ExitQP) { for (const [pid, qp] of stage3ExitQP) {
qpMap.set(pid, qp); qpMap.set(pid, qp);
} }
// Stage 1 and 2 exits get 0 QP for (const t of stage1EliminatedFinal) qpMap.set(t.id, 0);
for (const t of stage1Result.eliminated) qpMap.set(t.id, 0); for (const t of stage2EliminatedFinal) qpMap.set(t.id, 0);
for (const t of stage2Result.eliminated) qpMap.set(t.id, 0);
return qpMap; return qpMap;
} }