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
|
||||
.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,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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<Record<string, string>>(() => {
|
||||
|
|
@ -131,6 +129,13 @@ export default function AdminCs2Setup() {
|
|||
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
|
||||
const byStage: Record<number, typeof stageResults> = { 1: [], 2: [], 3: [] };
|
||||
for (const r of stageResults) {
|
||||
|
|
@ -257,12 +262,12 @@ export default function AdminCs2Setup() {
|
|||
</h3>
|
||||
<div className="space-y-1">
|
||||
{teamsInStage.map(r => {
|
||||
const isEliminated = r.stageEliminated !== null;
|
||||
const isEliminated = eliminatedChecked[r.participantId] ?? false;
|
||||
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">
|
||||
<span className="text-sm flex items-center gap-2">
|
||||
{r.participantName}
|
||||
{isEliminated && (
|
||||
{r.stageEliminated !== null && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
eliminated {r.stageEliminatedWins}-3
|
||||
</Badge>
|
||||
|
|
@ -271,23 +276,20 @@ export default function AdminCs2Setup() {
|
|||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
defaultChecked={isEliminated}
|
||||
onChange={e => {
|
||||
// Show/hide the wins selector
|
||||
const winsEl = document.getElementById(`wins_${r.participantId}`);
|
||||
if (winsEl) winsEl.style.display = e.target.checked ? 'block' : 'none';
|
||||
// Also show/hide the hidden input
|
||||
const hiddenEl = document.getElementById(`elim_hidden_${r.participantId}`) as HTMLInputElement | null;
|
||||
if (hiddenEl) hiddenEl.disabled = !e.target.checked;
|
||||
}}
|
||||
checked={isEliminated}
|
||||
onChange={e =>
|
||||
setEliminatedChecked(prev => ({
|
||||
...prev,
|
||||
[r.participantId]: e.target.checked,
|
||||
}))
|
||||
}
|
||||
className="rounded"
|
||||
/>
|
||||
Eliminated
|
||||
</label>
|
||||
<div id={`wins_${r.participantId}`} style={{ display: isEliminated ? 'block' : 'none' }}>
|
||||
{isEliminated && (
|
||||
<select
|
||||
name={`elim_${r.participantId}`}
|
||||
id={`elim_hidden_${r.participantId}`}
|
||||
defaultValue={r.stageEliminatedWins?.toString() ?? '0'}
|
||||
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>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
{!isEliminated && <div />}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -230,6 +230,14 @@ export default function EventResults({
|
|||
</Link>
|
||||
</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 ? (
|
||||
<Badge variant="default" className="bg-emerald-500">
|
||||
<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.
|
||||
* 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<number, number>
|
||||
): 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 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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue