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:
parent
a516dedb76
commit
a0deb376a1
5 changed files with 108 additions and 49 deletions
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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 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) {
|
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 9–16), 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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue