diff --git a/scripts/reseed-ratings.ts b/scripts/reseed-ratings.ts index b03210e..2f64d41 100644 --- a/scripts/reseed-ratings.ts +++ b/scripts/reseed-ratings.ts @@ -80,43 +80,40 @@ async function main() { const [acc] = await sql`SELECT account_id FROM account WHERE user_id = ${r.user_id} AND provider_id = 'osu' LIMIT 1`; + let rank: number | null = null; + if (!acc) { - console.log(` [SKIP] user ${r.user_id} — no osu! account linked`); - skipped++; - continue; - } - - try { - const osuUser = await getOsuUser(acc.account_id); - const rank = osuUser?.statistics?.global_rank; - - if (!rank || rank <= 0) { - console.log(` [SKIP] user ${r.user_id} (osu! ${acc.account_id}) — unranked`); + console.log(` [WARN] user ${r.user_id} — no osu! account linked, treating as unranked`); + } else { + try { + const osuUser = await getOsuUser(acc.account_id); + rank = osuUser?.statistics?.global_rank ?? null; + } catch (err: any) { + console.error(` [ERR] user ${r.user_id} (osu! ${acc.account_id}):`, err.message); skipped++; continue; } - - const seedElo = rankToElo(rank); - // Adjust by win/loss differential to preserve some earned progress - const adjustment = (r.wins - r.losses) * 12; - const finalElo = Math.max(0, Math.min(3500, seedElo + adjustment)); - - await sql`UPDATE player_rating SET - elo = ${finalElo}, - initial_elo = ${seedElo}, - osu_rank_at_seed = ${rank}, - updated_at = now() - WHERE id = ${r.id}`; - - console.log( - ` [OK] user ${r.user_id} — rank #${rank.toLocaleString()} → seed ${seedElo}, adj ${adjustment > 0 ? '+' : ''}${adjustment} → final ${finalElo} (was ${r.elo})` - ); - updated++; - } catch (err: any) { - console.error(` [ERR] user ${r.user_id} (osu! ${acc.account_id}):`, err.message); - skipped++; } + // Unranked → rank 100,000 (~1000 ELO) + const effectiveRank = rank && rank > 0 ? rank : 100_000; + const seedElo = rankToElo(effectiveRank); + // Adjust by win/loss differential to preserve some earned progress + const adjustment = (r.wins - r.losses) * 12; + const finalElo = Math.max(0, Math.min(3500, seedElo + adjustment)); + + await sql`UPDATE player_rating SET + elo = ${finalElo}, + initial_elo = ${seedElo}, + osu_rank_at_seed = ${rank}, + updated_at = now() + WHERE id = ${r.id}`; + + console.log( + ` [OK] user ${r.user_id} — rank #${rank ? rank.toLocaleString() : 'unranked (10M)'} → seed ${seedElo}, adj ${adjustment > 0 ? '+' : ''}${adjustment} → final ${finalElo} (was ${r.elo})` + ); + updated++; + // Rate limit: 500ms between API calls await new Promise((resolve) => setTimeout(resolve, 500)); } diff --git a/src/routes/(app)/admin/+page.server.ts b/src/routes/(app)/admin/+page.server.ts index eb55270..4989851 100644 --- a/src/routes/(app)/admin/+page.server.ts +++ b/src/routes/(app)/admin/+page.server.ts @@ -12,8 +12,10 @@ import { matchParticipant, matchParticipantPlayer } from '$lib/server/db/schema'; -import { eq, desc, inArray, sql } from 'drizzle-orm'; +import { account } from '$lib/server/db/auth.schema'; +import { eq, and, desc, inArray, sql } from 'drizzle-orm'; import { requireRole, setUserRole, isRootAdmin } from '$lib/server/permissions'; +import { getUser as getOsuUser } from '$lib/server/osu/api'; import type { PageServerLoad, Actions } from './$types'; export const load: PageServerLoad = async ({ locals }) => { @@ -127,11 +129,40 @@ export const actions: Actions = { resetRatings: async ({ locals }) => { requireRole(locals, 'admin'); - const reset = await db - .update(playerRating) - .set({ elo: sql`COALESCE(initial_elo, 1000)`, wins: 0, losses: 0, updatedAt: new Date() }) - .returning({ id: playerRating.id }); - return { success: true, message: `Reset ${reset.length} rating${reset.length !== 1 ? 's' : ''} to initial ELO` }; + + const allUsers = await db.query.user.findMany(); + let updated = 0; + + for (const u of allUsers) { + let rank: number | null = null; + + try { + const osuAccount = await db.query.account.findFirst({ + where: and(eq(account.userId, u.id), eq(account.providerId, 'osu')) + }); + if (osuAccount?.accountId) { + const osuUser = await getOsuUser(osuAccount.accountId); + rank = osuUser?.statistics?.global_rank ?? null; + } + } catch { + // API failure — treat as unranked + } + + // Unranked → rank 100,000 (~1000 ELO) + const effectiveRank = rank && rank > 0 ? rank : 100_000; + const rawElo = 3500 - Math.log10(effectiveRank) * 500; + const elo = Math.round(Math.max(0, Math.min(3500, rawElo))); + + await db + .update(playerRating) + .set({ elo, initialElo: elo, osuRankAtSeed: rank, wins: 0, losses: 0, updatedAt: new Date() }) + .where(eq(playerRating.userId, u.id)); + + console.log(`[Admin] Reset ${u.name}: rank #${rank ?? 'unranked'} → ${elo} ELO`); + updated++; + } + + return { success: true, message: `Re-seeded ${updated} rating${updated !== 1 ? 's' : ''} from osu! ranks` }; }, clearMatchHistory: async ({ locals }) => { diff --git a/src/routes/(app)/admin/+page.svelte b/src/routes/(app)/admin/+page.svelte index efd2981..a720e84 100644 --- a/src/routes/(app)/admin/+page.svelte +++ b/src/routes/(app)/admin/+page.svelte @@ -148,7 +148,7 @@ { action: 'clearQueue', label: 'Clear Queue', confirm: 'Remove everyone from the queue?', color: 'border-border text-text-secondary hover:bg-surface-700' }, { action: 'expireInvites', label: 'Expire All Invites', confirm: 'Expire all pending invites?', color: 'border-border text-text-secondary hover:bg-surface-700' }, { action: 'clearNotifications', label: 'Clear Notifications', confirm: 'Delete all notifications?', color: 'border-border text-text-secondary hover:bg-surface-700' }, - { action: 'resetRatings', label: 'Reset Ratings', confirm: 'Reset all ELO ratings to initial values? This cannot be undone.', color: 'border-red-500/30 text-red-400 hover:bg-red-500/10' }, + { action: 'resetRatings', label: 'Re-seed Ratings from osu!', confirm: 'Fetch all osu! ranks and re-seed ELO ratings? W/L records will be reset. Unranked players get 10M rank. This cannot be undone.', color: 'border-red-500/30 text-red-400 hover:bg-red-500/10' }, { action: 'clearMatchHistory', label: 'Clear All Match Data', confirm: 'Delete ALL matches, scores, invites, and notifications? This cannot be undone.', color: 'border-red-500/30 text-red-400 hover:bg-red-500/10' }, ] as tool}