Remove unused scripts

This commit is contained in:
Kalle
2026-06-12 21:24:52 +03:00
parent 51808edcc3
commit 08e29d41f9
23 changed files with 3 additions and 1909 deletions

View File

@@ -143,7 +143,7 @@ function calculateSkills(args: {
return result;
}
export function calculateIndividualPlayerSkills({
function calculateIndividualPlayerSkills({
results,
queryCurrentUserRating,
}: {
@@ -645,7 +645,7 @@ function spDiffs({
return spDiffs;
}
export function setResults({
function setResults({
results,
teams,
}: {

View File

@@ -15,7 +15,7 @@ export const TIER_THRESHOLDS = {
C: Number.NEGATIVE_INFINITY,
} as const;
export const TOP_TEAMS_COUNT = 8;
const TOP_TEAMS_COUNT = 8;
export const MIN_TEAMS_FOR_TIERING = 8;
export const TIER_HISTORY_LENGTH = 5;

View File

@@ -1,15 +0,0 @@
import * as AdminRepository from "~/features/admin/AdminRepository.server";
import { logger } from "~/utils/logger";
async function main() {
const input = process.argv[2]?.trim();
const userIds = input.split(",").map((id) => Number(id));
for (const userId of userIds) {
await AdminRepository.makeTournamentOrganizerByUserId(userId);
}
logger.info(`Added TOs: ${userIds}`);
}
main();

View File

@@ -1,135 +0,0 @@
import { db } from "../app/db/sql";
import * as Seasons from "../app/features/mmr/core/Seasons";
import {
queryCurrentTeamRating,
queryCurrentUserRating,
queryCurrentUserSeedingRating,
queryTeamPlayerRatingAverage,
} from "../app/features/mmr/mmr-utils.server";
import * as Standings from "../app/features/tournament/core/Standings";
import { tournamentSummary } from "../app/features/tournament-bracket/core/summarizer.server";
import { tournamentFromDB } from "../app/features/tournament-bracket/core/Tournament.server";
import * as TournamentMatchRepository from "../app/features/tournament-match/TournamentMatchRepository.server";
import invariant from "../app/utils/invariant";
import { logger } from "../app/utils/logger";
async function main() {
logger.info("Starting to backfill tournament result divisions");
const tournaments = await db
.selectFrom("Tournament")
.select("id")
.where("isFinalized", "=", 1)
.execute();
let recalculatedCount = 0;
let skippedCount = 0;
for (const { id: tournamentId } of tournaments) {
try {
const tournament = await tournamentFromDB({
tournamentId,
user: undefined,
});
const uniqueStartingBracketIndexes = new Set(
tournament.ctx.teams
.map((team) => team.startingBracketIdx)
.filter((idx) => idx !== null && idx !== undefined),
);
if (uniqueStartingBracketIndexes.size <= 1) {
skippedCount++;
continue;
}
recalculatedCount++;
await db
.deleteFrom("TournamentResult")
.where("tournamentId", "=", tournamentId)
.execute();
const results =
await TournamentMatchRepository.allResultsByTournamentId(tournamentId);
invariant(results.length > 0, "No results found");
const season = Seasons.current(tournament.ctx.startTime)?.nth;
const seedingSkillCountsFor = tournament.skillCountsFor;
const standingsResult = Standings.tournamentStandings(tournament);
if (standingsResult.type === "single") {
throw new Error(
`Expected multiple starting brackets for tournament ${tournamentId}`,
);
}
const finalStandings = Standings.flattenStandings(standingsResult);
const summary = tournamentSummary({
teams: tournament.ctx.teams,
finalStandings,
results,
calculateSeasonalStats: false,
queryCurrentTeamRating: (identifier) =>
queryCurrentTeamRating({ identifier, season: season! }).rating,
queryCurrentUserRating: (userId) =>
queryCurrentUserRating({ userId, season: season! }),
queryTeamPlayerRatingAverage: (identifier) =>
queryTeamPlayerRatingAverage({
identifier,
season: season!,
}),
queryCurrentSeedingRating: (userId) =>
queryCurrentUserSeedingRating({
userId,
type: seedingSkillCountsFor!,
}),
seedingSkillCountsFor,
progression: tournament.ctx.settings.bracketProgression,
});
logger.info(
`Inserting ${summary.tournamentResults.length} results for tournament ${tournamentId}`,
);
for (const tournamentResult of summary.tournamentResults) {
const setResults = summary.setResults.get(tournamentResult.userId);
if (setResults?.every((result) => !result)) {
continue;
}
await db
.insertInto("TournamentResult")
.values({
tournamentId,
userId: tournamentResult.userId,
placement: tournamentResult.placement,
participantCount: tournamentResult.participantCount,
tournamentTeamId: tournamentResult.tournamentTeamId,
setResults: setResults ? JSON.stringify(setResults) : "[]",
spDiff: null,
div: tournamentResult.div,
})
.execute();
}
if (recalculatedCount % 10 === 0) {
logger.info(
`Processed ${recalculatedCount} tournaments with multiple starting brackets (skipped ${skippedCount})`,
);
}
} catch (thrown) {
if (thrown instanceof Response) continue;
logger.error(`Error processing tournament ${tournamentId}`, thrown);
}
}
logger.info(
`Done. Recalculated ${recalculatedCount} tournaments with multiple starting brackets. Skipped ${skippedCount} tournaments.`,
);
}
main().catch((err) => {
logger.error("Error in backfill-tournament-result-divisions.ts", err);
process.exit(1);
});

View File

@@ -1,258 +0,0 @@
/** biome-ignore-all lint/suspicious/noConsole: CLI script output */
/**
* Backfill Tournament Tiers Script
*
* Run with: pnpm exec vite-node scripts/backfill-tournament-tiers.ts
*
* Retroactively calculates and sets tiers for all finalized tournaments,
* then populates series tier history based on those tiers.
*/
import { sql } from "~/db/sql";
import {
calculateAdjustedScore,
calculateTierNumber,
MIN_TEAMS_FOR_TIERING,
TIER_HISTORY_LENGTH,
TOP_TEAMS_COUNT,
} from "../app/features/tournament/core/tiering";
const dryRun = process.argv.includes("--dry-run");
interface TournamentScore {
tournamentId: number;
teamCount: number;
top8AvgOrdinal: number | null;
}
interface TournamentWithOrg {
tournamentId: number;
name: string;
organizationId: number | null;
startTime: number;
}
interface Series {
id: number;
organizationId: number;
substringMatches: string;
}
function getTournamentScores(): TournamentScore[] {
const query = `
WITH TeamSkills AS (
SELECT
tt.tournamentId,
tt.id as teamId,
AVG(ss.ordinal) as avg_team_ordinal
FROM TournamentTeam tt
JOIN TournamentTeamMember ttm ON ttm.tournamentTeamId = tt.id
LEFT JOIN SeedingSkill ss ON ss.userId = ttm.userId AND ss.type = 'RANKED'
WHERE tt.droppedOut = 0
GROUP BY tt.tournamentId, tt.id
),
TeamCounts AS (
SELECT tournamentId, COUNT(*) as team_count
FROM TeamSkills
WHERE avg_team_ordinal IS NOT NULL
GROUP BY tournamentId
),
RankedTeams AS (
SELECT
ts.tournamentId,
ts.avg_team_ordinal,
tc.team_count,
ROW_NUMBER() OVER (PARTITION BY ts.tournamentId ORDER BY ts.avg_team_ordinal DESC) as rank
FROM TeamSkills ts
JOIN TeamCounts tc ON tc.tournamentId = ts.tournamentId
WHERE ts.avg_team_ordinal IS NOT NULL
),
TournamentScores AS (
SELECT
tournamentId,
AVG(avg_team_ordinal) as top8_avg_ordinal,
MAX(team_count) as team_count
FROM RankedTeams
WHERE rank <= ${TOP_TEAMS_COUNT}
GROUP BY tournamentId
)
SELECT
t.id as tournamentId,
COALESCE(ts.team_count, 0) as teamCount,
ts.top8_avg_ordinal as top8AvgOrdinal
FROM Tournament t
LEFT JOIN TournamentScores ts ON ts.tournamentId = t.id
WHERE t.isFinalized = 1
`;
return sql.prepare(query).all() as TournamentScore[];
}
function getTournamentsWithOrg(): TournamentWithOrg[] {
const query = /* sql */ `
SELECT
t.id as tournamentId,
ce.name,
ce.organizationId,
ced.startTime
FROM Tournament t
INNER JOIN CalendarEvent ce ON ce.tournamentId = t.id
INNER JOIN CalendarEventDate ced ON ced.eventId = ce.id
WHERE t.isFinalized = 1
AND ce.hidden = 0
ORDER BY ced.startTime ASC
`;
return sql.prepare(query).all() as TournamentWithOrg[];
}
function getAllSeries(): Series[] {
const query = /* sql */ `
SELECT id, organizationId, substringMatches
FROM TournamentOrganizationSeries
`;
return sql.prepare(query).all() as Series[];
}
function matchesSubstring(
eventName: string,
substringMatches: string[],
): boolean {
const eventNameLower = eventName.toLowerCase();
return substringMatches.some((match) =>
eventNameLower.includes(match.toLowerCase()),
);
}
function main() {
console.log("=== Backfilling Tournament Tiers ===\n");
if (dryRun) {
console.log("DRY RUN - no changes will be made\n");
}
const tournaments = getTournamentScores();
console.log(`Found ${tournaments.length} finalized tournaments\n`);
const updateTierStatement = sql.prepare(
/* sql */ `UPDATE "Tournament" SET tier = @tier WHERE id = @tournamentId`,
);
const tierCounts: Record<string, number> = {};
const tournamentTiers = new Map<number, number>();
let updatedCount = 0;
let skippedCount = 0;
for (const t of tournaments) {
const meetsMinTeams = t.teamCount >= MIN_TEAMS_FOR_TIERING;
let tierNumber: number | null = null;
if (t.top8AvgOrdinal !== null && meetsMinTeams) {
const adjustedScore = calculateAdjustedScore(
t.top8AvgOrdinal,
t.teamCount,
);
tierNumber = calculateTierNumber(adjustedScore);
}
if (tierNumber !== null) {
tierCounts[tierNumber] = (tierCounts[tierNumber] || 0) + 1;
tournamentTiers.set(t.tournamentId, tierNumber);
updatedCount++;
} else {
skippedCount++;
}
if (!dryRun) {
updateTierStatement.run({
tier: tierNumber,
tournamentId: t.tournamentId,
});
}
}
console.log("Tier distribution:");
const tierNames: Record<number, string> = {
1: "X",
2: "S+",
3: "S",
4: "A+",
5: "A",
6: "B+",
7: "B",
8: "C+",
9: "C",
};
for (let i = 1; i <= 9; i++) {
console.log(` ${tierNames[i]}: ${tierCounts[i] || 0}`);
}
console.log(`\nUpdated: ${updatedCount} tournaments`);
console.log(`Skipped (untiered): ${skippedCount} tournaments`);
console.log("\n=== Backfilling Series Tier History ===\n");
const allSeries = getAllSeries();
const tournamentsWithOrg = getTournamentsWithOrg();
console.log(`Found ${allSeries.length} series`);
console.log(
`Found ${tournamentsWithOrg.filter((t) => t.organizationId !== null).length} tournaments with organizations\n`,
);
const updateSeriesStatement = sql.prepare(
/* sql */ "UPDATE TournamentOrganizationSeries SET tierHistory = @tierHistory WHERE id = @seriesId",
);
const seriesByOrg = new Map<number, Series[]>();
for (const series of allSeries) {
const existing = seriesByOrg.get(series.organizationId) ?? [];
existing.push(series);
seriesByOrg.set(series.organizationId, existing);
}
let seriesUpdatedCount = 0;
let seriesSkippedCount = 0;
for (const [organizationId, orgSeries] of seriesByOrg.entries()) {
const orgTournaments = tournamentsWithOrg.filter(
(t) => t.organizationId === organizationId,
);
for (const series of orgSeries) {
const substringMatches = JSON.parse(series.substringMatches) as string[];
const matchingTournaments = orgTournaments
.filter((t) => matchesSubstring(t.name, substringMatches))
.filter((t) => tournamentTiers.has(t.tournamentId));
if (matchingTournaments.length === 0) {
seriesSkippedCount++;
continue;
}
const tierHistory = matchingTournaments
.slice(-TIER_HISTORY_LENGTH)
.map((t) => tournamentTiers.get(t.tournamentId)!);
console.log(
`Series ${series.id} (org ${organizationId}): ${matchingTournaments.length} matching tournaments, tierHistory = [${tierHistory.join(", ")}]`,
);
if (!dryRun) {
updateSeriesStatement.run({
seriesId: series.id,
tierHistory: JSON.stringify(tierHistory),
});
}
seriesUpdatedCount++;
}
}
console.log(`\nSeries updated: ${seriesUpdatedCount}`);
console.log(
`Series skipped (no matching tournaments): ${seriesSkippedCount}`,
);
if (dryRun) {
console.log("\nRun without --dry-run to apply changes");
}
}
main();

View File

@@ -1,89 +0,0 @@
import { ordinal, type Rating, rating } from "openskill";
import { db } from "../app/db/sql";
import type { Tables } from "../app/db/tables";
import { calculateIndividualPlayerSkills } from "../app/features/tournament-bracket/core/summarizer.server";
import { tournamentFromDB } from "../app/features/tournament-bracket/core/Tournament.server";
import * as TournamentMatchRepository from "../app/features/tournament-match/TournamentMatchRepository.server";
import invariant from "../app/utils/invariant";
import { logger } from "../app/utils/logger";
async function main() {
const result: Tables["SeedingSkill"][] = [];
for (const type of ["RANKED", "UNRANKED"] as const) {
const ratings = new Map<number, Rating>();
let count = 0;
for await (const tournament of tournaments(type)) {
count++;
const results = await TournamentMatchRepository.allResultsByTournamentId(
tournament.ctx.id,
);
invariant(results.length > 0, "No results found");
const skills = calculateIndividualPlayerSkills({
queryCurrentUserRating(userId) {
return { rating: ratings.get(userId) ?? rating(), matchesCount: 0 };
},
results,
});
for (const { userId, mu, sigma } of skills) {
ratings.set(userId, rating({ mu, sigma }));
}
}
logger.info(`Processed ${count} tournaments`);
for (const [userId, { mu, sigma }] of ratings) {
result.push({
mu,
sigma,
ordinal: ordinal(rating({ mu, sigma })),
type,
userId,
});
}
}
await db.transaction().execute(async (trx) => {
await trx.deleteFrom("SeedingSkill").execute();
for (const skill of result) {
await trx.insertInto("SeedingSkill").values(skill).execute();
}
});
logger.info(`Done. Total of ${result.length} seeding skills inserted`);
}
async function* tournaments(type: "RANKED" | "UNRANKED") {
const maxId = await db
.selectFrom("Tournament")
.select(({ fn }) => fn.max("id").as("maxId"))
.executeTakeFirstOrThrow()
.then((row) => row.maxId);
for (let tournamentId = 1; tournamentId <= maxId; tournamentId++) {
try {
const tournament = await tournamentFromDB({
tournamentId,
user: undefined,
});
if (!tournament.ctx.isFinalized) {
continue;
}
if (tournament.skillCountsFor === "RANKED" && type === "RANKED") {
yield tournament;
} else if (
tournament.skillCountsFor === "UNRANKED" &&
type === "UNRANKED"
) {
yield tournament;
}
} catch {
// logger.info(`Skipped tournament with id ${tournamentId}`);
}
}
}
main();

View File

@@ -1,191 +0,0 @@
import { sql } from "kysely";
import { db } from "../app/db/sql";
import {
setResults,
type TournamentSummary,
} from "../app/features/tournament-bracket/core/summarizer.server";
import { tournamentFromDB } from "../app/features/tournament-bracket/core/Tournament.server";
import * as TournamentMatchRepository from "../app/features/tournament-match/TournamentMatchRepository.server";
import invariant from "../app/utils/invariant";
import { logger } from "../app/utils/logger";
async function main() {
logger.info(
"Starting to fix tournamentTeamId in TournamentMatchGameResultParticipant",
);
await tournamentTeamIdsToTournamentMatchGameResultParticipantTable();
logger.info("Fixed tournamentTeamId in TournamentMatchGameResultParticipant");
const result: Array<
{ tournamentId: number } & Pick<TournamentSummary, "setResults">
> = [];
let count = 0;
for await (const tournament of tournaments()) {
count++;
const results = await TournamentMatchRepository.allResultsByTournamentId(
tournament.ctx.id,
);
invariant(results.length > 0, "No results found");
result.push({
tournamentId: tournament.ctx.id,
setResults: setResults({ results, teams: tournament.ctx.teams }),
});
if (count % 100 === 0) {
logger.info(`Processed ${count} tournaments`);
}
}
await db.transaction().execute(async (trx) => {
await trx
.updateTable("TournamentResult")
.set({
setResults: JSON.stringify([]),
})
.execute();
for (const { tournamentId, setResults } of result) {
for (const [userId, setResult] of setResults.entries()) {
await trx
.updateTable("TournamentResult")
.set({
setResults: JSON.stringify(setResult),
})
.where("tournamentId", "=", tournamentId)
.where("userId", "=", userId)
.execute();
}
}
});
logger.info(`Done. Total of ${result.length} results inserted.`);
await wipeEmptyResults();
}
async function* tournaments() {
const maxId = await db
.selectFrom("Tournament")
.select(({ fn }) => fn.max("id").as("maxId"))
.executeTakeFirstOrThrow()
.then((row) => row.maxId);
for (let tournamentId = 1; tournamentId <= maxId; tournamentId++) {
if (tournamentId === 1483) {
// broken one
continue;
}
try {
const tournament = await tournamentFromDB({
tournamentId,
user: undefined,
});
if (!tournament.ctx.isFinalized) {
continue;
}
yield tournament;
} catch (thrown) {
if (thrown instanceof Response) continue;
throw thrown;
}
}
}
// https://github.com/sendou-ink/sendou.ink/commit/96781122e2c5f9cd90564c9b57a45b74557fc400
async function tournamentTeamIdsToTournamentMatchGameResultParticipantTable() {
await db
.updateTable("TournamentMatchGameResultParticipant")
.set((eb) => ({
tournamentTeamId: eb
.selectFrom("TournamentTeamMember")
.innerJoin(
"TournamentTeam",
"TournamentTeamMember.tournamentTeamId",
"TournamentTeam.id",
)
// exclude teams that have not checked in
.innerJoin(
"TournamentTeamCheckIn",
"TournamentTeamCheckIn.tournamentTeamId",
"TournamentTeam.id",
)
.select("TournamentTeam.id")
.whereRef(
"TournamentTeamMember.userId",
"=",
"TournamentMatchGameResultParticipant.userId",
)
.whereRef(
"TournamentTeam.tournamentId",
"=",
eb
.selectFrom("TournamentMatchGameResult")
.innerJoin(
"TournamentMatch",
"TournamentMatchGameResult.matchId",
"TournamentMatch.id",
)
.innerJoin(
"TournamentStage",
"TournamentStage.id",
"TournamentMatch.stageId",
)
.innerJoin(
"Tournament",
"Tournament.id",
"TournamentStage.tournamentId",
)
.whereRef(
"TournamentMatchGameResult.id",
"=",
"TournamentMatchGameResultParticipant.matchGameResultId",
)
.select("Tournament.id")
.limit(1),
),
}))
.where("TournamentMatchGameResultParticipant.tournamentTeamId", "is", null)
.execute();
// manual fixes, not sure why these are needed
await db
.updateTable("TournamentMatchGameResultParticipant")
.set({
tournamentTeamId: 13077,
})
.where("userId", "=", 44085)
.where("tournamentTeamId", "is", null)
.execute();
await db
.updateTable("TournamentMatchGameResultParticipant")
.set({
tournamentTeamId: 14589,
})
.where("userId", "=", 10585)
.where("tournamentTeamId", "is", null)
.execute();
}
async function wipeEmptyResults() {
logger.info("Wiping empty results from TournamentResult table...");
const { numDeletedRows } = await db
.deleteFrom("TournamentResult")
.where(sql<boolean>`instr(setResults, 'W') = 0`)
.where(sql<boolean>`instr(setResults, 'L') = 0`)
.executeTakeFirst();
logger.info(
`Wiped ${numDeletedRows} empty results from TournamentResult table.`,
);
}
main().catch((err) => {
logger.error("Error in calc-tournament-summary-result-arrays.ts", err);
process.exit(1);
});

View File

@@ -1,34 +0,0 @@
import { db } from "~/db/sql";
import { logger } from "~/utils/logger";
async function main() {
const skills = await db
.selectFrom("Skill")
.leftJoin("TournamentResult", (join) =>
join
.onRef("TournamentResult.tournamentId", "=", "Skill.tournamentId")
.onRef("TournamentResult.userId", "=", "Skill.userId"),
)
.select(["Skill.id"])
.where("Skill.tournamentId", "is not", null)
.where("TournamentResult.tournamentId", "is", null)
.execute();
logger.info(`Found ${skills.length} skills without results`);
await db
.updateTable("Skill")
.set({
tournamentId: null,
})
.where(
"id",
"in",
skills.map((skill) => skill.id),
)
.execute();
logger.info("Deleted skills without results");
}
void main();

View File

@@ -1,23 +0,0 @@
import { sql } from "~/db/sql";
import invariant from "~/utils/invariant";
import { logger } from "~/utils/logger";
const rawTournamentTeamId = process.argv[2]?.trim();
invariant(rawTournamentTeamId, "tournament team is required (argument 1)");
const tournamentTeamId = Number(rawTournamentTeamId);
invariant(
!Number.isNaN(tournamentTeamId),
"tournament team id must be a number",
);
const deleteMapPoolStm = sql.prepare(/*sql*/ `
delete from "MapPoolMap"
where "tournamentTeamId" = @tournamentTeamId
`);
deleteMapPoolStm.run({ tournamentTeamId });
logger.info(`Deleted map pool of tournament team with id: ${tournamentTeamId}`);

View File

@@ -1,23 +0,0 @@
import { db } from "~/db/sql";
import { dateToDatabaseTimestamp } from "~/utils/dates";
import invariant from "~/utils/invariant";
import { logger } from "~/utils/logger";
const id = Number(process.argv[2]?.trim());
invariant(id, "team id is required (argument 1)");
invariant(Number.isInteger(id), "team id must be an integer");
async function main() {
await db
.updateTable("AllTeam")
.set({
deletedAt: dateToDatabaseTimestamp(new Date()),
})
.where("id", "=", id)
.execute();
logger.info(`Disbanded team with id: ${id}`);
}
void main();

View File

@@ -1,37 +0,0 @@
import { db } from "~/db/sql";
import invariant from "~/utils/invariant";
import { logger } from "~/utils/logger";
const friendCode = process.argv[2]?.trim();
invariant(friendCode, "friend code is required (argument 1)");
async function main() {
const allFcs = await db
.selectFrom("UserFriendCode")
.innerJoin("User", "User.id", "UserFriendCode.userId")
.select([
"UserFriendCode.friendCode",
"User.id as userId",
"User.discordId",
"User.discordUniqueName",
])
.orderBy("UserFriendCode.createdAt", "asc")
.whereRef("User.id", "=", "UserFriendCode.submitterUserId")
.execute();
const matches = allFcs.filter((fc) => fc.friendCode === friendCode);
if (matches.length === 0) {
logger.info("No matches found");
return;
}
for (const match of matches) {
logger.info(
`${match.friendCode} - ${match.discordUniqueName} - ${match.discordId}`,
);
}
}
void main();

View File

@@ -1,126 +0,0 @@
import { db } from "~/db/sql";
import { BANNED_MAPS } from "~/features/match-profile/banned-maps";
import { modesShort } from "~/modules/in-game-lists/modes";
import { stageIds } from "~/modules/in-game-lists/stage-ids";
import type { ModeShort, StageId } from "~/modules/in-game-lists/types";
import {
databaseTimestampToDate,
dateToDatabaseTimestamp,
} from "~/utils/dates";
import { logger } from "~/utils/logger";
import names from "../locales/en/game-misc.json";
const SEASON_2_START = new Date("2023-12-04T17:00:00.000Z");
async function main() {
const appearance = await db
.selectFrom("GroupMatchMap")
.innerJoin("GroupMatch", "GroupMatchMap.matchId", "GroupMatch.id")
.select(({ fn }) => [
"GroupMatchMap.mode",
"GroupMatchMap.stageId",
fn.countAll<number>().as("count"),
])
.groupBy(["GroupMatchMap.stageId", "GroupMatchMap.mode"])
.where("GroupMatch.createdAt", ">", dateToDatabaseTimestamp(SEASON_2_START))
.execute();
const usage: Record<
ModeShort | "ALL",
{ stageId: StageId; count: number }[]
> = {
TW: [],
SZ: [],
TC: [],
RM: [],
CB: [],
ALL: [],
};
const ageRow = await db
.selectFrom("Build")
.select((eb) => eb.fn.max("Build.updatedAt").as("age"))
.executeTakeFirstOrThrow();
const dbAgeDate = databaseTimestampToDate(ageRow.age);
for (const mode of modesShort) {
for (const stageId of stageIds) {
const count =
appearance.find((row) => row.stageId === stageId && row.mode === mode)
?.count ?? 0;
usage[mode].push({
stageId,
count,
});
const existingAllCount = usage.ALL.find((row) => row.stageId === stageId);
if (!existingAllCount) {
usage.ALL.push({
stageId,
count,
});
} else {
existingAllCount.count += count;
}
}
usage[mode].sort((a, b) => b.count - a.count);
}
usage.ALL.sort((a, b) => b.count - a.count);
logger.info(`DB Age: ${dbAgeDate.toISOString()}\n`);
// all modes
logger.info("All");
let banCount = 0;
for (const [i, { stageId, count }] of usage.ALL.entries()) {
const name = names[`STAGE_${stageId}`];
const partlyBanned = Object.values(BANNED_MAPS).some((arr) =>
arr.includes(stageId as any),
);
if (partlyBanned) banCount++;
logger.info(
`${i < 9 ? " " : ""}${i + 1}) ${
partlyBanned ? "🔴" : " "
} ${name}: ${count}`,
);
}
logger.info(`Banned maps (at least one mode): ${banCount}`);
logger.info();
// modes
for (const mode of modesShort) {
// if (usage[mode].every((e) => e.count === 0)) continue;
logger.info(mode);
let banCount = 0;
for (const [i, { stageId, count }] of usage[mode].entries()) {
const name = names[`STAGE_${stageId}`];
const isBanned = BANNED_MAPS[mode].includes(stageId);
if (isBanned) banCount++;
logger.info(
`${i < 9 ? " " : ""}${i + 1}) ${
isBanned ? "❌" : " "
} ${name}: ${count}`,
);
}
logger.info(`Banned maps: ${banCount}`);
logger.info(
Object.values(usage[mode]).reduce((acc, cur) => acc + cur.count, 0),
);
}
}
void main();

View File

@@ -1,108 +0,0 @@
import { db } from "~/db/sql";
import { BANNED_MAPS } from "~/features/match-profile/banned-maps";
import { modesShort } from "~/modules/in-game-lists/modes";
import { stageIds } from "~/modules/in-game-lists/stage-ids";
import type { ModeShort, StageId } from "~/modules/in-game-lists/types";
import { databaseTimestampToDate } from "~/utils/dates";
import { logger } from "~/utils/logger";
import { cutToNDecimalPlaces } from "~/utils/number";
import names from "../locales/en/game-misc.json";
async function main() {
const appearance = await db
.selectFrom("MapPoolMap")
.select(({ fn }) => [
"MapPoolMap.stageId",
"MapPoolMap.mode",
fn.countAll<number>().as("count"),
])
.where("MapPoolMap.calendarEventId", "is not", null)
.groupBy(["MapPoolMap.stageId", "MapPoolMap.mode"])
.execute();
const usage: Record<
ModeShort,
{ stageId: StageId; count: number; relativeCount: number }[]
> = {
TW: [],
SZ: [],
TC: [],
RM: [],
CB: [],
};
const ageRow = await db
.selectFrom("Build")
.select((eb) => eb.fn.max("Build.updatedAt").as("age"))
.executeTakeFirstOrThrow();
const dbAgeDate = databaseTimestampToDate(ageRow.age);
for (const mode of modesShort) {
for (const stageId of stageIds) {
const count =
appearance.find((row) => row.stageId === stageId && row.mode === mode)
?.count ?? 0;
const firstAppear = await db
.selectFrom("MapPoolMap")
.innerJoin(
"CalendarEvent",
"MapPoolMap.calendarEventId",
"CalendarEvent.id",
)
.innerJoin(
"CalendarEventDate",
"CalendarEvent.id",
"CalendarEventDate.eventId",
)
.select((eb) =>
eb.fn.min("CalendarEventDate.startTime").as("firstAppear"),
)
.executeTakeFirst();
const firstAppearDate = firstAppear
? databaseTimestampToDate(firstAppear.firstAppear)
: null;
const datesSinceFirstAppear = firstAppearDate
? Math.floor((dbAgeDate.getTime() - firstAppearDate.getTime()) / 864e5)
: null;
usage[mode].push({
stageId,
count,
relativeCount: datesSinceFirstAppear
? cutToNDecimalPlaces((count / datesSinceFirstAppear) * 30, 3)
: 0,
});
}
usage[mode].sort((a, b) => b.relativeCount - a.relativeCount);
}
logger.info(`DB Age: ${dbAgeDate.toISOString()}\n`);
for (const mode of modesShort) {
logger.info(mode);
let banCount = 0;
for (const [i, { stageId, count, relativeCount }] of usage[
mode
].entries()) {
const name = names[`STAGE_${stageId}`];
const isBanned = BANNED_MAPS[mode].includes(stageId);
if (isBanned) banCount++;
logger.info(
`${i < 9 ? " " : ""}${i + 1}) ${
isBanned ? "❌" : " "
} ${name}: ${relativeCount} (${count})`,
);
}
logger.info(`Banned maps: ${banCount}`);
logger.info();
}
}
void main();

View File

@@ -1,170 +0,0 @@
import fs from "node:fs";
import path from "node:path";
import { Readable } from "node:stream";
import { db } from "~/db/sql";
import { uploadStreamToS3 } from "~/features/img-upload/s3.server";
import { databaseTimestampNow } from "~/utils/dates";
import invariant from "~/utils/invariant";
import { logger } from "~/utils/logger";
const TOURNAMENT_LOGO_PATH = "public/static-assets/img/tournament-logos";
const ADMIN_ID = 1;
const LOGO_IDENTIFIER_TO_PATTERN: Record<string, string[]> = {
sf: ["sendouq"],
pp: ["paddling pool"],
itz: ["in the zone"],
pn: ["picnic"],
pg: ["proving grounds"],
tc: ["triton"],
sos: ["swim or sink"],
ftiu: ["from the ink up"],
cc: ["coral clash"],
lu: ["level up"],
a41: ["all 4 one"],
fb: ["fry basket"],
d: ["the depths"],
e: ["eclipse"],
hc: ["homecoming"],
bio: ["bad ideas"],
ai: ["tenoch"],
mm: ["megalodon monday"],
ho: ["heaven 2 ocean"],
kr: ["kraken royale"],
mr: ["menu royale"],
bc: ["barracuda co"],
ci: ["crimson ink"],
me: ["mesozoic mayhem"],
ros: ["rain or shine"],
sj: ["squid junction"],
ss: ["silly sausage"],
ul: ["united-lan"],
sc: ["soul cup"],
};
async function uploadLogoFile(
filePath: string,
identifier: string,
): Promise<string> {
logger.info(`Uploading ${identifier}.png to S3...`);
const fileBuffer = fs.readFileSync(filePath);
const stream = Readable.from(fileBuffer);
const fileName = `tournament-logo-${identifier}.png`;
const s3Url = await uploadStreamToS3(stream, fileName);
invariant(s3Url, `Failed to upload ${identifier}.png to S3`);
logger.info(`Uploaded ${identifier}.png to ${s3Url}`);
return fileName;
}
async function createImageRecord(url: string): Promise<number> {
const result = await db
.insertInto("UnvalidatedUserSubmittedImage")
.values({
url,
validatedAt: databaseTimestampNow(),
submitterUserId: ADMIN_ID,
})
.returning("id")
.executeTakeFirstOrThrow();
return result.id;
}
async function updateCalendarEvents(
imageId: number,
patterns: string[],
): Promise<number> {
const events = await db
.selectFrom("CalendarEvent")
.select(["id", "name"])
.where("avatarImgId", "is", null)
.execute();
let updateCount = 0;
for (const event of events) {
const normalizedEventName = event.name.toLowerCase();
const matches = patterns.some((pattern) =>
normalizedEventName.includes(pattern),
);
if (matches) {
await db
.updateTable("CalendarEvent")
.set({ avatarImgId: imageId })
.where("id", "=", event.id)
.execute();
logger.info(`Updated CalendarEvent ${event.id}: "${event.name}"`);
updateCount++;
}
}
return updateCount;
}
async function main() {
logger.info("Starting tournament logo migration to S3...");
const defaultLogoPath = path.join(TOURNAMENT_LOGO_PATH, "default.png");
if (fs.existsSync(defaultLogoPath)) {
logger.info("\n=== Uploading default tournament logo ===");
const defaultS3Url = await uploadLogoFile(defaultLogoPath, "default");
const defaultImageId = await createImageRecord(defaultS3Url);
logger.info(
`Created UnvalidatedUserSubmittedImage record for default logo with ID ${defaultImageId}\n`,
);
}
const logoFiles = fs
.readdirSync(TOURNAMENT_LOGO_PATH)
.filter((file) => file.endsWith(".png") && file !== "default.png");
logger.info(`Found ${logoFiles.length} logo files to migrate`);
let totalUpdated = 0;
for (const file of logoFiles) {
const identifier = file.replace(".png", "");
const patterns = LOGO_IDENTIFIER_TO_PATTERN[identifier];
if (!patterns) {
logger.warn(`No pattern mapping found for ${identifier}, skipping...`);
continue;
}
const filePath = path.join(TOURNAMENT_LOGO_PATH, file);
const s3Url = await uploadLogoFile(filePath, identifier);
const imageId = await createImageRecord(s3Url);
logger.info(
`Created UnvalidatedUserSubmittedImage record with ID ${imageId}`,
);
const updatedCount = await updateCalendarEvents(imageId, patterns);
totalUpdated += updatedCount;
logger.info(
`Updated ${updatedCount} CalendarEvent records for ${identifier}\n`,
);
}
logger.info("\n=== Migration Complete ===");
logger.info(`Total CalendarEvent records updated: ${totalUpdated}`);
}
main()
.then(() => {
logger.info("Script completed successfully");
process.exit(0);
})
.catch((error) => {
logger.error("Script failed:", error);
process.exit(1);
});

View File

@@ -1,44 +0,0 @@
import { db } from "~/db/sql";
import * as Seasons from "~/features/mmr/core/Seasons";
import * as SQMatchRepository from "~/features/sendouq-match/SQMatchRepository.server";
import { dateToDatabaseTimestamp } from "~/utils/dates";
import { logger } from "~/utils/logger";
async function main() {
if (Seasons.current()) {
throw new Error("There is a current season");
}
if (Seasons.next()?.nth !== 8) {
throw new Error("Next season is not 8. Script needs modifying");
}
const wrongSeasonStartDate = new Date("2025-06-14T18:00:00.000Z");
const allMatches = await db
.selectFrom("GroupMatch")
.selectAll()
.where(
"GroupMatch.createdAt",
">=",
dateToDatabaseTimestamp(wrongSeasonStartDate),
)
.execute();
await db
.deleteFrom("Skill")
.where(
"Skill.groupMatchId",
"in",
allMatches.map((m) => m.id),
)
.execute();
for (const match of allMatches) {
await SQMatchRepository.lockMatchWithoutSkillChange(match.id);
}
logger.info(`All done with nuking the season (${allMatches.length} matches)`);
}
void main();

View File

@@ -1,58 +0,0 @@
import { db } from "~/db/sql";
import { databaseTimestampToDate } from "~/utils/dates";
import { logger } from "~/utils/logger";
async function main() {
const allFcs = await db
.selectFrom("UserFriendCode")
.innerJoin("User", "User.id", "UserFriendCode.userId")
.select(["UserFriendCode.friendCode", "User.id as userId"])
.orderBy("UserFriendCode.createdAt", "asc")
.whereRef("User.id", "=", "UserFriendCode.submitterUserId")
.execute();
const fcMap = new Map<string, number[]>();
for (const fc of allFcs) {
const fcs = fcMap.get(fc.friendCode) ?? [];
fcs.push(fc.userId);
fcMap.set(fc.friendCode, fcs);
}
const friendCodeAdders = await db
.selectFrom("UserFriendCode")
.innerJoin("User", "User.id", "UserFriendCode.userId")
.select([
"UserFriendCode.friendCode",
"UserFriendCode.createdAt",
"User.id",
"User.discordId",
"User.discordUniqueName",
])
.orderBy("UserFriendCode.createdAt", "desc")
.whereRef("User.id", "=", "UserFriendCode.submitterUserId")
.limit(90)
.execute();
let result = "";
let date = "";
for (const [i, friendCodeAdder] of friendCodeAdders.entries()) {
const utc = databaseTimestampToDate(
friendCodeAdder.createdAt,
).toUTCString();
const newDate = utc.split(",")[0];
if (date !== newDate) {
date = newDate;
result += "\n";
}
const isDuplicate =
(fcMap.get(friendCodeAdder.friendCode) ?? [])?.length > 1;
result += `${i < 9 ? "0" : ""}${i + 1}) ${utc} - ${friendCodeAdder.friendCode}${isDuplicate ? " >>DUPLICATE<<" : ""} - ${friendCodeAdder.discordUniqueName} - ${friendCodeAdder.discordId}\n`;
}
logger.info(result);
}
void main();

View File

@@ -1,18 +0,0 @@
/** biome-ignore-all lint/suspicious/noConsole: CLI script */
import * as SplatoonRotationRepository from "~/features/splatoon-rotations/SplatoonRotationRepository.server";
import { fetchRotations } from "~/features/splatoon-rotations/splatoon-rotations.server";
async function main() {
console.log("Fetching splatoon rotations from splatoon3.ink...");
const rotations = await fetchRotations();
console.log(`Fetched ${rotations.length} rotations`);
await SplatoonRotationRepository.replaceAll(rotations);
console.log("Rotations synced to database");
}
main().catch((err) => {
console.error("Failed to sync rotations:", err);
process.exit(1);
});

View File

@@ -1,14 +0,0 @@
// usage: npx vite-node ./scripts/sync-tournament-vods.ts <tournamentId>
import { processOneTournament } from "~/routines/syncTournamentVods";
import invariant from "~/utils/invariant";
import { logger } from "~/utils/logger";
const tournamentId = Number(process.argv[2]?.trim());
invariant(
tournamentId && !Number.isNaN(tournamentId),
"tournament id is required (argument 1)",
);
logger.info(`Syncing VODs for tournament ${tournamentId}...`);
await processOneTournament(tournamentId);
logger.info("Done");

View File

@@ -1,7 +0,0 @@
import * as BadgeRepository from "~/features/badges/BadgeRepository.server";
import { logger } from "~/utils/logger";
void (async () => {
await BadgeRepository.syncXPBadges();
logger.info("Synced XP badges");
})();

View File

@@ -1,406 +0,0 @@
/** biome-ignore-all lint/suspicious/noConsole: CLI script output */
/**
* Tournament Tiering Experiment Script
*
* Run with: pnpm exec vite-node scripts/tournament-tiers-experiment.ts
*
* Calculates tournament tiers based on top teams' average SeedingSkill.
* Tweak the THRESHOLDS object to experiment with different tier distributions.
*/
import Database from "better-sqlite3";
const db = new Database("db-prod.sqlite3", { readonly: true });
// ============================================================================
// CONFIGURATION - Tweak these values to experiment
// ============================================================================
/**
* Minimum ordinal thresholds for each tier.
* A tournament is assigned the highest tier where its score meets the threshold.
*
* Current values are based on percentile analysis:
* - X: Top ~1% of tournaments
* - S+: Top ~3%
* - S: Top ~8%
* - A+: Top ~15%
* - A: Top ~25%
* - B+: Top ~40%
* - B: Top ~55%
* - C+: Top ~75%
* - C: Everything else
*/
const THRESHOLDS = {
X: 32,
"S+": 29,
S: 26,
"A+": 24,
A: 21,
"B+": 15,
B: 10,
"C+": 5,
C: Number.NEGATIVE_INFINITY, // Catch-all
} as const;
/**
* How many top teams to consider for the tournament score.
* Using 8 captures the competitive core of most tournaments.
*/
const TOP_TEAMS_COUNT = 8;
/**
* Minimum number of teams required for a tournament to be tiered.
* Tournaments with fewer teams will be marked as "UNTIERED".
*/
const MIN_TEAMS_FOR_TIERING = 8;
/**
* Size bonus configuration.
* The bonus scales inversely with skill level - X-tier gets no bonus,
* lower tiers get increasingly more bonus for larger tournaments.
*
* NO_BONUS_ABOVE: Score threshold above which no size bonus applies (X-tier)
* MAX_BONUS_PER_10_TEAMS: Maximum bonus per 10 teams above minimum (applied at score 0)
*
* The bonus scales linearly: at NO_BONUS_ABOVE, multiplier is 0.
* As score decreases toward 0, multiplier approaches MAX_BONUS_PER_10_TEAMS.
*
* Formula: bonus = scaleFactor * MAX_BONUS * (teamsAboveMin / 10)
* where scaleFactor = max(0, (NO_BONUS_ABOVE - rawScore) / NO_BONUS_ABOVE)
*/
const SIZE_BONUS = {
NO_BONUS_ABOVE: 32, // X-tier threshold - no bonus at this level
MAX_BONUS_PER_10_TEAMS: 1.5, // Max points added per 10 extra teams (at score ~0)
};
/**
* Filter to only include tournaments after this date (ISO string).
* Set to null to include all tournaments.
*/
const MIN_DATE: string | null = null; // e.g., "2024-01-01"
// ============================================================================
// IMPLEMENTATION
// ============================================================================
type Tier = keyof typeof THRESHOLDS;
interface TournamentData {
tournamentId: number;
eventId: number;
name: string;
startTime: number;
teamCount: number;
top8AvgOrdinal: number | null;
adjustedScore: number | null;
tier: Tier | "UNTIERED";
}
function calculateTier(score: number | null): Tier | "UNTIERED" {
if (score === null) return "UNTIERED";
const tiers = Object.entries(THRESHOLDS) as [Tier, number][];
for (const [tier, threshold] of tiers) {
if (score >= threshold) return tier;
}
return "C";
}
function calculateAdjustedScore(rawScore: number, teamCount: number): number {
if (SIZE_BONUS.MAX_BONUS_PER_10_TEAMS === 0) return rawScore;
// Scale factor: 0 at NO_BONUS_ABOVE, approaches 1 as score approaches 0
const scaleFactor = Math.max(
0,
(SIZE_BONUS.NO_BONUS_ABOVE - rawScore) / SIZE_BONUS.NO_BONUS_ABOVE,
);
const teamsAboveMin = Math.max(0, teamCount - MIN_TEAMS_FOR_TIERING);
const bonus =
scaleFactor * SIZE_BONUS.MAX_BONUS_PER_10_TEAMS * (teamsAboveMin / 10);
return rawScore + bonus;
}
function getTournamentData(): TournamentData[] {
const query = `
WITH TeamSkills AS (
SELECT
tt.tournamentId,
tt.id as teamId,
AVG(ss.ordinal) as avg_team_ordinal
FROM TournamentTeam tt
JOIN TournamentTeamMember ttm ON ttm.tournamentTeamId = tt.id
LEFT JOIN SeedingSkill ss ON ss.userId = ttm.userId AND ss.type = 'RANKED'
WHERE tt.droppedOut = 0
GROUP BY tt.tournamentId, tt.id
),
TeamCounts AS (
SELECT tournamentId, COUNT(*) as team_count
FROM TeamSkills
WHERE avg_team_ordinal IS NOT NULL
GROUP BY tournamentId
),
RankedTeams AS (
SELECT
ts.tournamentId,
ts.avg_team_ordinal,
tc.team_count,
ROW_NUMBER() OVER (PARTITION BY ts.tournamentId ORDER BY ts.avg_team_ordinal DESC) as rank
FROM TeamSkills ts
JOIN TeamCounts tc ON tc.tournamentId = ts.tournamentId
WHERE ts.avg_team_ordinal IS NOT NULL
),
TournamentScores AS (
SELECT
tournamentId,
AVG(avg_team_ordinal) as top8_avg_ordinal,
MAX(team_count) as team_count
FROM RankedTeams
WHERE rank <= ${TOP_TEAMS_COUNT}
GROUP BY tournamentId
)
SELECT
t.id as tournamentId,
ce.id as eventId,
ce.name,
ced.startTime,
ts.team_count as teamCount,
ts.top8_avg_ordinal as top8AvgOrdinal
FROM Tournament t
JOIN CalendarEvent ce ON ce.tournamentId = t.id
JOIN CalendarEventDate ced ON ced.eventId = ce.id
LEFT JOIN TournamentScores ts ON ts.tournamentId = t.id
WHERE t.isFinalized = 1
${MIN_DATE ? `AND ced.startTime >= strftime('%s', '${MIN_DATE}') * 1000` : ""}
GROUP BY t.id
ORDER BY ced.startTime DESC
`;
const rows = db.prepare(query).all() as Array<{
tournamentId: number;
eventId: number;
name: string;
startTime: number;
teamCount: number | null;
top8AvgOrdinal: number | null;
}>;
return rows.map((row) => {
const teamCount = row.teamCount ?? 0;
const meetsMinTeams = teamCount >= MIN_TEAMS_FOR_TIERING;
let adjustedScore: number | null = null;
if (row.top8AvgOrdinal !== null && meetsMinTeams) {
adjustedScore = calculateAdjustedScore(row.top8AvgOrdinal, teamCount);
}
return {
tournamentId: row.tournamentId,
eventId: row.eventId,
name: row.name,
startTime: row.startTime,
teamCount,
top8AvgOrdinal: row.top8AvgOrdinal,
adjustedScore,
tier: meetsMinTeams ? calculateTier(adjustedScore) : "UNTIERED",
};
});
}
function printDistribution(tournaments: TournamentData[]) {
const distribution: Record<string, number> = {};
const tiers = [...Object.keys(THRESHOLDS), "UNTIERED"];
for (const tier of tiers) {
distribution[tier] = 0;
}
for (const t of tournaments) {
distribution[t.tier]++;
}
const total = tournaments.length;
const tiered = total - distribution.UNTIERED;
console.log(`\n${"=".repeat(60)}`);
console.log("TIER DISTRIBUTION");
console.log("=".repeat(60));
console.log(`Total tournaments: ${total}`);
console.log(`Tiered (${MIN_TEAMS_FOR_TIERING}+ teams): ${tiered}`);
console.log(
`Untiered (< ${MIN_TEAMS_FOR_TIERING} teams): ${distribution.UNTIERED}`,
);
console.log("-".repeat(60));
for (const tier of tiers) {
if (tier === "UNTIERED") continue;
const count = distribution[tier];
const pctOfTiered = tiered > 0 ? ((count / tiered) * 100).toFixed(1) : "0";
const bar = "█".repeat(Math.round(count / 20));
console.log(
`${tier.padEnd(3)} | ${String(count).padStart(4)} | ${pctOfTiered.padStart(5)}% of tiered | ${bar}`,
);
}
}
function formatDate(timestamp: number): string {
// timestamps are stored in seconds, not milliseconds
return new Date(timestamp * 1000).toISOString().split("T")[0];
}
function printTopTournaments(
tournaments: TournamentData[],
tier: Tier,
limit = 10,
) {
const filtered = tournaments
.filter((t) => t.tier === tier)
.sort((a, b) => (b.adjustedScore ?? 0) - (a.adjustedScore ?? 0))
.slice(0, limit);
console.log(`\n${"=".repeat(60)}`);
console.log(`TOP ${limit} ${tier}-TIER TOURNAMENTS`);
console.log("=".repeat(60));
for (const t of filtered) {
const date = formatDate(t.startTime);
console.log(
`[${date}] ${t.name.substring(0, 40).padEnd(40)} | ${t.teamCount} teams | score: ${t.adjustedScore?.toFixed(1)}`,
);
}
}
function printBottomOfTier(
tournaments: TournamentData[],
tier: Tier,
limit = 5,
) {
const filtered = tournaments
.filter((t) => t.tier === tier)
.sort((a, b) => (a.adjustedScore ?? 0) - (b.adjustedScore ?? 0))
.slice(0, limit);
console.log(`\n${"-".repeat(60)}`);
console.log(`BOTTOM ${limit} OF ${tier}-TIER (borderline)`);
console.log("-".repeat(60));
for (const t of filtered) {
const date = formatDate(t.startTime);
console.log(
`[${date}] ${t.name.substring(0, 40).padEnd(40)} | ${t.teamCount} teams | score: ${t.adjustedScore?.toFixed(1)}`,
);
}
}
function printThresholds() {
console.log(`\n${"=".repeat(60)}`);
console.log("CURRENT THRESHOLDS");
console.log("=".repeat(60));
for (const [tier, threshold] of Object.entries(THRESHOLDS)) {
if (threshold === Number.NEGATIVE_INFINITY) {
console.log(`${tier}: < ${THRESHOLDS["C+"]}`);
} else {
console.log(`${tier}: >= ${threshold}`);
}
}
console.log(`\nTop teams considered: ${TOP_TEAMS_COUNT}`);
console.log(`Min teams for tiering: ${MIN_TEAMS_FOR_TIERING}`);
console.log("\nSize bonus (scales inversely with skill):");
console.log(` No bonus above score: ${SIZE_BONUS.NO_BONUS_ABOVE}`);
console.log(
` Max bonus per 10 teams: ${SIZE_BONUS.MAX_BONUS_PER_10_TEAMS} points`,
);
// Show example bonus calculations
console.log("\nExample bonuses for 50-team tournament:");
const exampleTeams = 50;
for (const rawScore of [32, 28, 24, 20, 15, 10, 5, 0]) {
const adjusted = calculateAdjustedScore(rawScore, exampleTeams);
const bonus = adjusted - rawScore;
console.log(
` Raw ${rawScore.toString().padStart(2)} -> ${adjusted.toFixed(2)} (+${bonus.toFixed(2)})`,
);
}
}
// ============================================================================
// MAIN
// ============================================================================
function main() {
console.log("Tournament Tiering Experiment");
console.log("==============================\n");
printThresholds();
const tournaments = getTournamentData();
printDistribution(tournaments);
// Show examples from top tiers
printTopTournaments(tournaments, "X", 15);
printBottomOfTier(tournaments, "X", 5);
printTopTournaments(tournaments, "S+", 10);
printBottomOfTier(tournaments, "S+", 5);
printTopTournaments(tournaments, "S", 10);
// Show tournaments promoted by size bonus
console.log(`\n${"=".repeat(60)}`);
console.log("TOURNAMENTS PROMOTED BY SIZE BONUS");
console.log("=".repeat(60));
const promoted = tournaments
.filter((t) => t.tier !== "UNTIERED" && t.top8AvgOrdinal !== null)
.filter((t) => {
const rawTier = calculateTier(t.top8AvgOrdinal);
return rawTier !== t.tier;
})
.sort((a, b) => (b.adjustedScore ?? 0) - (a.adjustedScore ?? 0))
.slice(0, 20);
if (promoted.length === 0) {
console.log("No tournaments were promoted by size bonus.");
} else {
for (const t of promoted) {
const rawTier = calculateTier(t.top8AvgOrdinal);
const bonus = (t.adjustedScore ?? 0) - (t.top8AvgOrdinal ?? 0);
console.log(
`${rawTier.toString().padEnd(3)} -> ${t.tier.padEnd(3)} | ${t.name.substring(0, 35).padEnd(35)} | ${t.teamCount} teams | +${bonus.toFixed(2)}`,
);
}
}
// Recent tournaments analysis
console.log(`\n${"=".repeat(60)}`);
console.log("RECENT TOURNAMENTS (last 30 tiered)");
console.log("=".repeat(60));
tournaments
.filter((t) => t.tier !== "UNTIERED")
.sort((a, b) => b.startTime - a.startTime)
.slice(0, 30)
.forEach((t) => {
const date = formatDate(t.startTime);
const safeName = t.name.substring(0, 35).padEnd(35);
console.log(
`${t.tier.padEnd(3)} | [${date}] ${safeName} | ${String(t.teamCount).padStart(3)} teams | ${t.adjustedScore?.toFixed(1)}`,
);
});
// Full CSV dump ordered by score
console.log(`\n${"=".repeat(60)}`);
console.log("FULL CSV DUMP (ordered by score descending)");
console.log("=".repeat(60));
console.log("name,score,tier");
tournaments
.filter((t) => t.tier !== "UNTIERED")
.sort((a, b) => (b.adjustedScore ?? 0) - (a.adjustedScore ?? 0))
.forEach((t) => {
const safeName = t.name.replace(/,/g, ";").replace(/"/g, "'");
console.log(`"${safeName}",${t.adjustedScore?.toFixed(2)},${t.tier}`);
});
}
main();
db.close();

View File

@@ -1,47 +0,0 @@
import { db } from "~/db/sql";
import { logger } from "~/utils/logger";
async function main() {
const weaponPools = await db
.selectFrom("UserWeapon")
.select([
"UserWeapon.userId",
"UserWeapon.weaponSplId",
"UserWeapon.userId",
])
.where("UserWeapon.order", "!=", 5)
.orderBy("UserWeapon.order", "asc")
.execute();
// group by userId
const weaponPoolsByUserId = weaponPools.reduce(
(acc, weaponPool) => {
if (!acc[weaponPool.userId]) {
acc[weaponPool.userId] = [];
}
acc[weaponPool.userId].push(weaponPool);
return acc;
},
{} as Record<string, typeof weaponPools>,
);
for (const [userId, weaponPools] of Object.entries(weaponPoolsByUserId)) {
const weaponPoolIds = weaponPools.map(
(weaponPool) => weaponPool.weaponSplId,
);
await db
.updateTable("User")
.set({
weaponPool: JSON.stringify(weaponPoolIds),
})
.where("User.id", "=", Number(userId))
.execute();
}
logger.info("done with the transfer");
}
void main();

View File

@@ -1,69 +0,0 @@
// usage: pnpm exec vite-node ./scripts/unlock-league-matches.ts <parentTournamentId>
import { sql } from "~/db/sql";
import invariant from "~/utils/invariant";
import { logger } from "~/utils/logger";
const parentTournamentId = Number(process.argv[2]?.trim());
invariant(
parentTournamentId && !Number.isNaN(parentTournamentId),
"parent tournament id is required (argument 1)",
);
const divisions = sql
.prepare(
'SELECT id FROM "Tournament" WHERE parentTournamentId = @parentTournamentId',
)
.all({ parentTournamentId }) as { id: number }[];
invariant(
divisions.length > 0,
`No divisions found for tournament ${parentTournamentId}`,
);
logger.info(`Found ${divisions.length} divisions`);
const updateMatch = sql.prepare(
'UPDATE "TournamentMatch" SET status = 2 WHERE id = @id',
);
const unlockAll = sql.transaction(() => {
let totalUnlocked = 0;
for (const division of divisions) {
const stages = sql
.prepare(
'SELECT id FROM "TournamentStage" WHERE tournamentId = @tournamentId',
)
.all({ tournamentId: division.id }) as { id: number }[];
if (stages.length === 0) continue;
const stageIds = stages.map((s) => s.id);
const lockedMatches = sql
.prepare(
`SELECT id, opponentOne, opponentTwo FROM "TournamentMatch"
WHERE stageId IN (${stageIds.map(() => "?").join(",")})
AND status = 0`,
)
.all(...stageIds) as {
id: number;
opponentOne: string;
opponentTwo: string;
}[];
let divUnlocked = 0;
for (const match of lockedMatches) {
const o1 = JSON.parse(match.opponentOne);
const o2 = JSON.parse(match.opponentTwo);
if (o1?.id == null || o2?.id == null) continue;
updateMatch.run({ id: match.id });
divUnlocked++;
}
logger.info(`Division ${division.id}: unlocked ${divUnlocked} match(es)`);
totalUnlocked += divUnlocked;
}
logger.info(`Total unlocked: ${totalUnlocked} match(es)`);
});
unlockAll();

View File

@@ -1,34 +0,0 @@
import { db } from "~/db/sql";
import invariant from "~/utils/invariant";
import { logger } from "~/utils/logger";
const rawEventId = process.argv[2]?.trim();
invariant(rawEventId, "eventId is required (argument 1)");
const eventId = Number(rawEventId);
invariant(!Number.isNaN(eventId), "eventId must be a number");
const newName = process.argv[3]?.trim();
invariant(newName, "newName is required (argument 2)");
async function main() {
const oldName = (
await db
.selectFrom("CalendarEvent")
.select(["CalendarEvent.name"])
.where("id", "=", eventId)
.executeTakeFirstOrThrow()
).name;
await db
.updateTable("CalendarEvent")
.set({ name: newName })
.where("CalendarEvent.id", "=", eventId)
.execute();
logger.info(
`Event name updated from "${oldName}" to "${newName}" for event ID: ${eventId}`,
);
}
main();