mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 18:04:39 -05:00
Many starting brackets standings (#2611)
This commit is contained in:
parent
2c25ef6561
commit
9fc30a7624
|
|
@ -600,6 +600,8 @@ export interface TournamentResult {
|
|||
/** The SP change in total after the finalization of a ranked tournament. */
|
||||
spDiff: number | null;
|
||||
userId: number;
|
||||
/** Division label for tournaments with multiple starting brackets (e.g., "D1", "D2") */
|
||||
div: string | null;
|
||||
}
|
||||
|
||||
export interface TournamentRoundMaps {
|
||||
|
|
|
|||
|
|
@ -65,9 +65,11 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
|
|||
const season = Seasons.current(tournament.ctx.startTime)?.nth;
|
||||
|
||||
const seedingSkillCountsFor = tournament.skillCountsFor;
|
||||
const standingsResult = Standings.tournamentStandings(tournament);
|
||||
const finalStandings = Standings.flattenStandings(standingsResult);
|
||||
const summary = tournamentSummary({
|
||||
teams: tournament.ctx.teams,
|
||||
finalStandings: Standings.tournamentStandings(tournament),
|
||||
finalStandings,
|
||||
results,
|
||||
calculateSeasonalStats: tournament.ranked,
|
||||
queryCurrentTeamRating: (identifier) =>
|
||||
|
|
@ -85,6 +87,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
|
|||
type: seedingSkillCountsFor!,
|
||||
}),
|
||||
seedingSkillCountsFor,
|
||||
progression: tournament.ctx.settings.bracketProgression,
|
||||
});
|
||||
|
||||
const tournamentSummaryString = `Tournament id: ${tournamentId}, mapResultDeltas.lenght: ${summary.mapResultDeltas.length}, playerResultDeltas.length ${summary.playerResultDeltas.length}, tournamentResults.length ${summary.tournamentResults.length}, skills.length ${summary.skills.length}, seedingSkills.length ${summary.seedingSkills.length}`;
|
||||
|
|
|
|||
|
|
@ -638,13 +638,13 @@ describe("isUnderground", () => {
|
|||
false,
|
||||
);
|
||||
expect(Progression.isUnderground(1, progressions.manyStartBrackets)).toBe(
|
||||
true,
|
||||
false,
|
||||
);
|
||||
expect(Progression.isUnderground(2, progressions.manyStartBrackets)).toBe(
|
||||
false,
|
||||
);
|
||||
expect(Progression.isUnderground(3, progressions.manyStartBrackets)).toBe(
|
||||
true,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -720,6 +720,26 @@ describe("bracketIdxsForStandings", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("startingBrackets", () => {
|
||||
it("handles SE", () => {
|
||||
expect(
|
||||
Progression.startingBrackets(progressions.singleElimination),
|
||||
).toEqual([0]);
|
||||
});
|
||||
|
||||
it("handles many starter brackets", () => {
|
||||
expect(
|
||||
Progression.startingBrackets(progressions.manyStartBrackets),
|
||||
).toEqual([0, 1]);
|
||||
});
|
||||
|
||||
it("handles swiss (one group)", () => {
|
||||
expect(Progression.startingBrackets(progressions.swissOneGroup)).toEqual([
|
||||
0,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("destinationsFromBracketIdx", () => {
|
||||
it("returns correct destination (one destination)", () => {
|
||||
expect(
|
||||
|
|
|
|||
|
|
@ -595,7 +595,17 @@ export function isFinals(idx: number, brackets: ParsedBracket[]) {
|
|||
export function isUnderground(idx: number, brackets: ParsedBracket[]) {
|
||||
invariant(idx < brackets.length, "Bracket index out of bounds");
|
||||
|
||||
return !resolveMainBracketProgression(brackets).includes(idx);
|
||||
const startBrackets = startingBrackets(brackets);
|
||||
|
||||
for (const startBracketIdx of startBrackets) {
|
||||
if (
|
||||
resolveMainBracketProgression(brackets, startBracketIdx).includes(idx)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -619,11 +629,14 @@ export function bracketDepth(idx: number, brackets: ParsedBracket[]): number {
|
|||
return Math.max(...sourceDepths) + 1;
|
||||
}
|
||||
|
||||
function resolveMainBracketProgression(brackets: ParsedBracket[]) {
|
||||
function resolveMainBracketProgression(
|
||||
brackets: ParsedBracket[],
|
||||
startBracketIdx = 0,
|
||||
) {
|
||||
if (brackets.length === 1) return [0];
|
||||
|
||||
let bracketIdxToFind = 0;
|
||||
const result = [0];
|
||||
let bracketIdxToFind = startBracketIdx;
|
||||
const result = [startBracketIdx];
|
||||
while (true) {
|
||||
const bracket = brackets.findIndex((bracket) =>
|
||||
bracket.sources?.some(
|
||||
|
|
@ -685,8 +698,11 @@ export function changedBracketProgressionFormat(
|
|||
return false;
|
||||
}
|
||||
|
||||
/** Returns the order of brackets as is to be considered for standings. Teams from the bracket of lower index are considered to be above those from the lower bracket.
|
||||
* A participant's standing is the first bracket to appear in order that has the participant in it.
|
||||
/**
|
||||
* Returns the order of brackets as is to be considered for standings. Teams from the bracket of lower index are considered to be above those from the lower bracket.
|
||||
* A participant's standing is the first bracket to appear in order that has the participant in it.
|
||||
*
|
||||
* The order is so that most significant brackets (i.e. finals) appear first.
|
||||
*/
|
||||
export function bracketIdxsForStandings(progression: ParsedBracket[]) {
|
||||
const bracketsToConsider = bracketsReachableFrom(0, progression);
|
||||
|
|
@ -734,7 +750,7 @@ export function bracketIdxsForStandings(progression: ParsedBracket[]) {
|
|||
});
|
||||
}
|
||||
|
||||
function bracketsReachableFrom(
|
||||
export function bracketsReachableFrom(
|
||||
bracketIdx: number,
|
||||
progression: ParsedBracket[],
|
||||
): number[] {
|
||||
|
|
@ -794,3 +810,10 @@ export function destinationByPlacement({
|
|||
|
||||
return destination ?? null;
|
||||
}
|
||||
|
||||
export function startingBrackets(progression: ParsedBracket[]): number[] {
|
||||
return progression
|
||||
.map((bracket, idx) => ({ bracket, idx }))
|
||||
.filter(({ bracket }) => !bracket.sources)
|
||||
.map(({ idx }) => idx);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,12 +7,14 @@ import {
|
|||
rate,
|
||||
userIdsToIdentifier,
|
||||
} from "~/features/mmr/mmr-utils";
|
||||
import { getBracketProgressionLabel } from "~/features/tournament/tournament-utils";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { roundToNDecimalPlaces } from "~/utils/number";
|
||||
import type { Tables, WinLossParticipationArray } from "../../../db/tables";
|
||||
import type { AllMatchResult } from "../queries/allMatchResultsByTournamentId.server";
|
||||
import { ensureOneStandingPerUser } from "../tournament-bracket-utils";
|
||||
import type { Standing } from "./Bracket";
|
||||
import type { ParsedBracket } from "./Progression";
|
||||
|
||||
export interface TournamentSummary {
|
||||
skills: Omit<
|
||||
|
|
@ -35,6 +37,7 @@ export interface TournamentSummary {
|
|||
type TeamsArg = Array<{
|
||||
id: number;
|
||||
members: Array<{ userId: number }>;
|
||||
startingBracketIdx?: number | null;
|
||||
}>;
|
||||
|
||||
type Rating = Pick<Tables["Skill"], "mu" | "sigma">;
|
||||
|
|
@ -53,6 +56,7 @@ export function tournamentSummary({
|
|||
queryCurrentSeedingRating,
|
||||
seedingSkillCountsFor,
|
||||
calculateSeasonalStats = true,
|
||||
progression,
|
||||
}: {
|
||||
results: AllMatchResult[];
|
||||
teams: TeamsArg;
|
||||
|
|
@ -63,6 +67,7 @@ export function tournamentSummary({
|
|||
queryCurrentSeedingRating: (userId: number) => Rating;
|
||||
seedingSkillCountsFor: Tables["SeedingSkill"]["type"] | null;
|
||||
calculateSeasonalStats?: boolean;
|
||||
progression: ParsedBracket[];
|
||||
}): TournamentSummary {
|
||||
const skills = calculateSeasonalStats
|
||||
? calculateSkills({
|
||||
|
|
@ -95,6 +100,8 @@ export function tournamentSummary({
|
|||
tournamentResults: tournamentResults({
|
||||
participantCount: teams.length,
|
||||
finalStandings: ensureOneStandingPerUser(finalStandings),
|
||||
teams,
|
||||
progression,
|
||||
}),
|
||||
spDiffs: calculateSeasonalStats
|
||||
? spDiffs({ skills, queryCurrentUserRating })
|
||||
|
|
@ -495,19 +502,37 @@ function playerResultDeltas(
|
|||
function tournamentResults({
|
||||
participantCount,
|
||||
finalStandings,
|
||||
teams,
|
||||
progression,
|
||||
}: {
|
||||
participantCount: number;
|
||||
finalStandings: Standing[];
|
||||
teams: TeamsArg;
|
||||
progression: ParsedBracket[];
|
||||
}) {
|
||||
const result: TournamentSummary["tournamentResults"] = [];
|
||||
|
||||
const firstPlaceFinishesCount = finalStandings.filter(
|
||||
(s) => s.placement === 1,
|
||||
).length;
|
||||
const isMultiStartingBracket = firstPlaceFinishesCount > 1;
|
||||
|
||||
for (const standing of finalStandings) {
|
||||
const team = teams.find((t) => t.id === standing.team.id);
|
||||
invariant(team);
|
||||
const div =
|
||||
// second check should be redundant, but just here in case
|
||||
typeof team.startingBracketIdx === "number" && isMultiStartingBracket
|
||||
? getBracketProgressionLabel(team.startingBracketIdx, progression)
|
||||
: null;
|
||||
|
||||
for (const player of standing.team.members) {
|
||||
result.push({
|
||||
participantCount,
|
||||
placement: standing.placement,
|
||||
tournamentTeamId: standing.team.id,
|
||||
userId: player.userId,
|
||||
div,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,13 +44,78 @@ describe("tournamentSummary()", () => {
|
|||
results,
|
||||
seedingSkillCountsFor,
|
||||
withMemberInTwoTeams = false,
|
||||
teamsWithStartingBrackets,
|
||||
progression,
|
||||
finalStandings,
|
||||
}: {
|
||||
results?: AllMatchResult[];
|
||||
seedingSkillCountsFor?: Tables["SeedingSkill"]["type"];
|
||||
withMemberInTwoTeams?: boolean;
|
||||
teamsWithStartingBrackets?: Array<{
|
||||
id: number;
|
||||
startingBracketIdx: number | null;
|
||||
}>;
|
||||
progression?: Array<{
|
||||
name: string;
|
||||
type: "single_elimination";
|
||||
settings: Record<string, never>;
|
||||
requiresCheckIn: boolean;
|
||||
sources?: Array<{ bracketIdx: number; placements: number[] }>;
|
||||
}>;
|
||||
finalStandings?: Array<{
|
||||
placement: number;
|
||||
team: TournamentDataTeam;
|
||||
}>;
|
||||
} = {}) {
|
||||
const defaultTeams = [
|
||||
{
|
||||
id: 1,
|
||||
members: [
|
||||
{ userId: 1 },
|
||||
{ userId: 2 },
|
||||
{ userId: 3 },
|
||||
{ userId: 4 },
|
||||
{ userId: 20 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
members: [{ userId: 5 }, { userId: 6 }, { userId: 7 }, { userId: 8 }],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
members: [
|
||||
{ userId: 9 },
|
||||
{ userId: 10 },
|
||||
{ userId: 11 },
|
||||
{ userId: 12 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
members: [
|
||||
{ userId: 13 },
|
||||
{ userId: 14 },
|
||||
{ userId: 15 },
|
||||
{ userId: 16 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const teams = teamsWithStartingBrackets
|
||||
? defaultTeams.map((team) => {
|
||||
const startingBracket = teamsWithStartingBrackets.find(
|
||||
(t) => t.id === team.id,
|
||||
);
|
||||
return {
|
||||
...team,
|
||||
startingBracketIdx: startingBracket?.startingBracketIdx ?? null,
|
||||
};
|
||||
})
|
||||
: defaultTeams;
|
||||
|
||||
return tournamentSummary({
|
||||
finalStandings: [
|
||||
finalStandings: finalStandings ?? [
|
||||
{
|
||||
placement: 1,
|
||||
team: createTeam(
|
||||
|
|
@ -117,45 +182,20 @@ describe("tournamentSummary()", () => {
|
|||
},
|
||||
},
|
||||
],
|
||||
teams: [
|
||||
{
|
||||
id: 1,
|
||||
members: [
|
||||
{ userId: 1 },
|
||||
{ userId: 2 },
|
||||
{ userId: 3 },
|
||||
{ userId: 4 },
|
||||
{ userId: 20 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
members: [{ userId: 5 }, { userId: 6 }, { userId: 7 }, { userId: 8 }],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
members: [
|
||||
{ userId: 9 },
|
||||
{ userId: 10 },
|
||||
{ userId: 11 },
|
||||
{ userId: 12 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
members: [
|
||||
{ userId: 13 },
|
||||
{ userId: 14 },
|
||||
{ userId: 15 },
|
||||
{ userId: 16 },
|
||||
],
|
||||
},
|
||||
],
|
||||
teams,
|
||||
queryCurrentTeamRating: () => rating(),
|
||||
queryCurrentUserRating: () => ({ rating: rating(), matchesCount: 0 }),
|
||||
queryTeamPlayerRatingAverage: () => rating(),
|
||||
queryCurrentSeedingRating: () => rating(),
|
||||
seedingSkillCountsFor: seedingSkillCountsFor ?? null,
|
||||
progression: progression ?? [
|
||||
{
|
||||
name: "Main Bracket",
|
||||
type: "single_elimination",
|
||||
settings: {},
|
||||
requiresCheckIn: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -620,4 +660,65 @@ describe("tournamentSummary()", () => {
|
|||
expect(results).toEqual(["W"]);
|
||||
}
|
||||
});
|
||||
|
||||
test("div is null when teams have no startingBracketIdx", () => {
|
||||
const summary = summarize();
|
||||
|
||||
for (const result of summary.tournamentResults) {
|
||||
expect(result.div).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
test("div is set correctly for teams with startingBracketIdx", () => {
|
||||
const summary = summarize({
|
||||
teamsWithStartingBrackets: [
|
||||
{ id: 1, startingBracketIdx: 0 },
|
||||
{ id: 2, startingBracketIdx: 1 },
|
||||
{ id: 3, startingBracketIdx: 0 },
|
||||
{ id: 4, startingBracketIdx: 1 },
|
||||
],
|
||||
progression: [
|
||||
{
|
||||
name: "Division 1",
|
||||
type: "single_elimination",
|
||||
settings: {},
|
||||
requiresCheckIn: false,
|
||||
},
|
||||
{
|
||||
name: "Division 2",
|
||||
type: "single_elimination",
|
||||
settings: {},
|
||||
requiresCheckIn: false,
|
||||
},
|
||||
],
|
||||
finalStandings: [
|
||||
{
|
||||
placement: 1,
|
||||
team: createTeam(1, [1, 2, 3, 4]),
|
||||
},
|
||||
{
|
||||
placement: 1,
|
||||
team: createTeam(2, [5, 6, 7, 8]),
|
||||
},
|
||||
{
|
||||
placement: 2,
|
||||
team: createTeam(3, [9, 10, 11, 12]),
|
||||
},
|
||||
{
|
||||
placement: 2,
|
||||
team: createTeam(4, [13, 14, 15, 16]),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const team1Results = summary.tournamentResults.filter(
|
||||
(r) => r.tournamentTeamId === 1,
|
||||
);
|
||||
const team2Results = summary.tournamentResults.filter(
|
||||
(r) => r.tournamentTeamId === 2,
|
||||
);
|
||||
|
||||
expect(team1Results.every((r) => r.div === "Division 1")).toBeTruthy();
|
||||
expect(team2Results.every((r) => r.div === "Division 2")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -54,7 +54,8 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
|||
};
|
||||
|
||||
async function standingsWithSetParticipation(tournament: Tournament) {
|
||||
const finalStandings = Standings.tournamentStandings(tournament);
|
||||
const standingsResult = Standings.tournamentStandings(tournament);
|
||||
const finalStandings = Standings.flattenStandings(standingsResult);
|
||||
|
||||
const results = allMatchResultsByTournamentId(tournament.ctx.id);
|
||||
invariant(results.length > 0, "No results found");
|
||||
|
|
@ -83,6 +84,7 @@ async function standingsWithSetParticipation(tournament: Tournament) {
|
|||
type: seedingSkillCountsFor!,
|
||||
}),
|
||||
seedingSkillCountsFor,
|
||||
progression: tournament.ctx.settings.bracketProgression,
|
||||
});
|
||||
|
||||
return finalStandings.map((standing) => {
|
||||
|
|
|
|||
|
|
@ -116,7 +116,8 @@ const addTournamentResultStm = sql.prepare(/* sql */ `
|
|||
"participantCount",
|
||||
"tournamentTeamId",
|
||||
"setResults",
|
||||
"spDiff"
|
||||
"spDiff",
|
||||
"div"
|
||||
) values (
|
||||
@tournamentId,
|
||||
@userId,
|
||||
|
|
@ -124,7 +125,8 @@ const addTournamentResultStm = sql.prepare(/* sql */ `
|
|||
@participantCount,
|
||||
@tournamentTeamId,
|
||||
@setResults,
|
||||
@spDiff
|
||||
@spDiff,
|
||||
@div
|
||||
)
|
||||
`);
|
||||
|
||||
|
|
@ -240,6 +242,7 @@ export const addSummary = sql.transaction(
|
|||
tournamentTeamId: tournamentResult.tournamentTeamId,
|
||||
setResults: setResults ? JSON.stringify(setResults) : null,
|
||||
spDiff: summary.spDiffs?.get(tournamentResult.userId) ?? null,
|
||||
div: tournamentResult.div,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,27 @@ import * as R from "remeda";
|
|||
import type { Standing } from "~/features/tournament-bracket/core/Bracket";
|
||||
import * as Progression from "~/features/tournament-bracket/core/Progression";
|
||||
import type { Tournament } from "~/features/tournament-bracket/core/Tournament";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { getBracketProgressionLabel } from "../tournament-utils";
|
||||
|
||||
export type TournamentStandingsResult =
|
||||
| { type: "single"; standings: Standing[] }
|
||||
| {
|
||||
type: "multi";
|
||||
standings: Array<{
|
||||
div: string;
|
||||
standings: Standing[];
|
||||
}>;
|
||||
};
|
||||
|
||||
/** Converts tournament standings from single or multi-division format into a flat array */
|
||||
export function flattenStandings(
|
||||
standingsResult: TournamentStandingsResult,
|
||||
): Standing[] {
|
||||
return standingsResult.type === "single"
|
||||
? standingsResult.standings
|
||||
: standingsResult.standings.flatMap((div) => div.standings);
|
||||
}
|
||||
|
||||
/** Calculates SPR (Seed Performance Rating) - see https://web.archive.org/web/20250513034545/https://www.pgstats.com/articles/introducing-spr-and-uf */
|
||||
export function calculateSPR({
|
||||
|
|
@ -92,12 +113,67 @@ export function matchesPlayed({
|
|||
* For example if the tournament format is round robin (where 2 out of 4 teams per group advance) to single elimination,
|
||||
* the top teams are decided by the single elimination bracket, and the teams who failed to make the bracket are ordered
|
||||
* by their performance in the round robin group stage.
|
||||
*
|
||||
* Returns a discriminated union:
|
||||
* - For tournaments with a single starting bracket, returns type 'single' with overall standings
|
||||
* - For tournaments with multiple starting brackets, returns type 'multi' with standings per division
|
||||
*/
|
||||
export function tournamentStandings(tournament: Tournament): Standing[] {
|
||||
const bracketIdxs = Progression.bracketIdxsForStandings(
|
||||
export function tournamentStandings(
|
||||
tournament: Tournament,
|
||||
): TournamentStandingsResult {
|
||||
const startingBracketIdxs = Progression.startingBrackets(
|
||||
tournament.ctx.settings.bracketProgression,
|
||||
);
|
||||
|
||||
if (startingBracketIdxs.length <= 1) {
|
||||
return {
|
||||
type: "single",
|
||||
standings: tournamentStandingsForBracket(tournament, undefined),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: "multi",
|
||||
standings: startingBracketIdxs.map((bracketIdx) => ({
|
||||
div: getBracketProgressionLabel(
|
||||
bracketIdx,
|
||||
tournament.ctx.settings.bracketProgression,
|
||||
),
|
||||
standings: tournamentStandingsForBracket(tournament, bracketIdx),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the standings for a given tournament starting from a specific bracket.
|
||||
* If bracketIdx is undefined, computes overall standings for the entire tournament.
|
||||
* Otherwise, only includes brackets that are reachable from the given bracketIdx.
|
||||
*/
|
||||
function tournamentStandingsForBracket(
|
||||
tournament: Tournament,
|
||||
bracketIdx: number | undefined,
|
||||
): Standing[] {
|
||||
let bracketIdxs: number[];
|
||||
|
||||
const isSingleStartingBracket = typeof bracketIdx !== "number";
|
||||
|
||||
if (isSingleStartingBracket) {
|
||||
bracketIdxs = Progression.bracketIdxsForStandings(
|
||||
tournament.ctx.settings.bracketProgression,
|
||||
);
|
||||
} else {
|
||||
const reachableBrackets = Progression.bracketsReachableFrom(
|
||||
bracketIdx,
|
||||
tournament.ctx.settings.bracketProgression,
|
||||
);
|
||||
const reachableSet = new Set(reachableBrackets);
|
||||
|
||||
const allBracketIdxs = tournament.ctx.settings.bracketProgression
|
||||
.map((_, idx) => idx)
|
||||
.sort((a, b) => b - a);
|
||||
bracketIdxs = allBracketIdxs.filter((idx) => reachableSet.has(idx));
|
||||
}
|
||||
|
||||
const result: Standing[] = [];
|
||||
const alreadyIncludedTeamIds = new Set<number>();
|
||||
|
||||
|
|
@ -105,11 +181,14 @@ export function tournamentStandings(tournament: Tournament): Standing[] {
|
|||
(bracket) => bracket.isFinals && bracket.everyMatchOver,
|
||||
);
|
||||
|
||||
for (const bracketIdx of bracketIdxs) {
|
||||
const bracket = tournament.bracketByIdx(bracketIdx);
|
||||
if (!bracket) continue;
|
||||
for (const idx of bracketIdxs) {
|
||||
const bracket = tournament.bracketByIdx(idx);
|
||||
invariant(bracket);
|
||||
|
||||
// sometimes a bracket might not be played so then we ignore it from the standings
|
||||
if (finalBracketIsOver && bracket.preview) continue;
|
||||
if (isSingleStartingBracket && finalBracketIsOver && bracket.preview) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const standings = standingsToMergeable({
|
||||
alreadyIncludedTeamIds,
|
||||
|
|
|
|||
|
|
@ -2,10 +2,17 @@ import { Link } from "@remix-run/react";
|
|||
import clsx from "clsx";
|
||||
import * as React from "react";
|
||||
import { Avatar } from "~/components/Avatar";
|
||||
import {
|
||||
SendouTab,
|
||||
SendouTabList,
|
||||
SendouTabPanel,
|
||||
SendouTabs,
|
||||
} from "~/components/elements/Tabs";
|
||||
import { Flag } from "~/components/Flag";
|
||||
import { InfoPopover } from "~/components/InfoPopover";
|
||||
import { Placement } from "~/components/Placement";
|
||||
import { Table } from "~/components/Table";
|
||||
import type { Standing } from "~/features/tournament-bracket/core/Bracket";
|
||||
import {
|
||||
SPR_INFO_URL,
|
||||
tournamentMatchPage,
|
||||
|
|
@ -17,120 +24,152 @@ import { useTournament } from "./to.$id";
|
|||
export default function TournamentResultsPage() {
|
||||
const tournament = useTournament();
|
||||
|
||||
const standings = Standings.tournamentStandings(tournament);
|
||||
const standingsResult = Standings.tournamentStandings(tournament);
|
||||
|
||||
if (standingsResult.type === "single") {
|
||||
if (standingsResult.standings.length === 0) {
|
||||
return (
|
||||
<div className="text-center text-lg font-semi-bold text-lighter">
|
||||
No team finished yet, check back later
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (standings.length === 0) {
|
||||
return (
|
||||
<div className="text-center text-lg font-semi-bold text-lighter">
|
||||
No team finished yet, check back later
|
||||
<div>
|
||||
<ResultsTable standings={standingsResult.standings} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SendouTabs>
|
||||
<SendouTabList>
|
||||
{standingsResult.standings.map(({ div }) => (
|
||||
<SendouTab key={div} id={div}>
|
||||
{div}
|
||||
</SendouTab>
|
||||
))}
|
||||
</SendouTabList>
|
||||
{standingsResult.standings.map(({ div, standings }) => (
|
||||
<SendouTabPanel key={div} id={div}>
|
||||
{standings.length === 0 ? (
|
||||
<div className="text-center text-lg font-semi-bold text-lighter">
|
||||
No team finished yet, check back later
|
||||
</div>
|
||||
) : (
|
||||
<ResultsTable standings={standings} />
|
||||
)}
|
||||
</SendouTabPanel>
|
||||
))}
|
||||
</SendouTabs>
|
||||
);
|
||||
}
|
||||
|
||||
function ResultsTable({ standings }: { standings: Standing[] }) {
|
||||
const tournament = useTournament();
|
||||
|
||||
let lastRenderedPlacement = 0;
|
||||
let rowDarkerBg = false;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Standing</th>
|
||||
<th>Team</th>
|
||||
<th>Roster</th>
|
||||
<th>Seed</th>
|
||||
{tournament.ctx.isFinalized ? (
|
||||
<th
|
||||
className="stack horizontal sm items-center"
|
||||
data-testid="spr-header"
|
||||
>
|
||||
SPR{" "}
|
||||
<InfoPopover tiny>
|
||||
<a
|
||||
href={SPR_INFO_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Standing</th>
|
||||
<th>Team</th>
|
||||
<th>Roster</th>
|
||||
<th>Seed</th>
|
||||
{tournament.ctx.isFinalized ? (
|
||||
<th
|
||||
className="stack horizontal sm items-center"
|
||||
data-testid="spr-header"
|
||||
>
|
||||
SPR{" "}
|
||||
<InfoPopover tiny>
|
||||
<a
|
||||
href={SPR_INFO_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Seed Performance Rating
|
||||
</a>
|
||||
</InfoPopover>
|
||||
</th>
|
||||
) : null}
|
||||
<th>Matches</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{standings.map((standing, i) => {
|
||||
const placement =
|
||||
lastRenderedPlacement === standing.placement
|
||||
? null
|
||||
: standing.placement;
|
||||
lastRenderedPlacement = standing.placement;
|
||||
|
||||
if (standing.placement !== standings[i - 1]?.placement) {
|
||||
rowDarkerBg = !rowDarkerBg;
|
||||
}
|
||||
|
||||
const teamLogoSrc = tournament.tournamentTeamLogoSrc(standing.team);
|
||||
|
||||
const spr = Standings.calculateSPR({
|
||||
standings,
|
||||
teamId: standing.team.id,
|
||||
});
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={standing.team.id}
|
||||
className={rowDarkerBg ? "bg-darker-transparent" : undefined}
|
||||
>
|
||||
<td className="text-md">
|
||||
{typeof placement === "number" ? (
|
||||
<Placement placement={placement} size={36} />
|
||||
) : null}{" "}
|
||||
</td>
|
||||
<td>
|
||||
<Link
|
||||
to={tournamentTeamPage({
|
||||
tournamentId: tournament.ctx.id,
|
||||
tournamentTeamId: standing.team.id,
|
||||
})}
|
||||
className="tournament__standings__team-name"
|
||||
data-testid="result-team-name"
|
||||
>
|
||||
{teamLogoSrc ? <Avatar size="xs" url={teamLogoSrc} /> : null}{" "}
|
||||
{standing.team.name}
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
{standing.team.members.map((player) => (
|
||||
<div
|
||||
key={player.userId}
|
||||
className="stack xxs horizontal items-center"
|
||||
>
|
||||
Seed Performance Rating
|
||||
</a>
|
||||
</InfoPopover>
|
||||
</th>
|
||||
) : null}
|
||||
<th>Matches</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{standings.map((standing, i) => {
|
||||
const placement =
|
||||
lastRenderedPlacement === standing.placement
|
||||
? null
|
||||
: standing.placement;
|
||||
lastRenderedPlacement = standing.placement;
|
||||
|
||||
if (standing.placement !== standings[i - 1]?.placement) {
|
||||
rowDarkerBg = !rowDarkerBg;
|
||||
}
|
||||
|
||||
const teamLogoSrc = tournament.tournamentTeamLogoSrc(standing.team);
|
||||
|
||||
const spr = Standings.calculateSPR({
|
||||
standings,
|
||||
teamId: standing.team.id,
|
||||
});
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={standing.team.id}
|
||||
className={rowDarkerBg ? "bg-darker-transparent" : undefined}
|
||||
>
|
||||
<td className="text-md">
|
||||
{typeof placement === "number" ? (
|
||||
<Placement placement={placement} size={36} />
|
||||
) : null}{" "}
|
||||
{player.country ? (
|
||||
<Flag countryCode={player.country} tiny />
|
||||
) : null}
|
||||
{player.username}
|
||||
</div>
|
||||
))}
|
||||
</td>
|
||||
<td className="text-sm">{standing.team.seed}</td>
|
||||
{tournament.ctx.isFinalized ? (
|
||||
<td className="text-sm">
|
||||
{spr > 0 ? "+" : ""}
|
||||
{spr}
|
||||
</td>
|
||||
<td>
|
||||
<Link
|
||||
to={tournamentTeamPage({
|
||||
tournamentId: tournament.ctx.id,
|
||||
tournamentTeamId: standing.team.id,
|
||||
})}
|
||||
className="tournament__standings__team-name"
|
||||
data-testid="result-team-name"
|
||||
>
|
||||
{teamLogoSrc ? (
|
||||
<Avatar size="xs" url={teamLogoSrc} />
|
||||
) : null}{" "}
|
||||
{standing.team.name}
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
{standing.team.members.map((player) => (
|
||||
<div
|
||||
key={player.userId}
|
||||
className="stack xxs horizontal items-center"
|
||||
>
|
||||
{player.country ? (
|
||||
<Flag countryCode={player.country} tiny />
|
||||
) : null}
|
||||
{player.username}
|
||||
</div>
|
||||
))}
|
||||
</td>
|
||||
<td className="text-sm">{standing.team.seed}</td>
|
||||
{tournament.ctx.isFinalized ? (
|
||||
<td className="text-sm">
|
||||
{spr > 0 ? "+" : ""}
|
||||
{spr}
|
||||
</td>
|
||||
) : null}
|
||||
<td>
|
||||
<MatchHistoryRow teamId={standing.team.id} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
) : null}
|
||||
<td>
|
||||
<MatchHistoryRow teamId={standing.team.id} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -107,7 +107,9 @@ function StatSquares({
|
|||
const data = useLoaderData<typeof loader>();
|
||||
const tournament = useTournament();
|
||||
|
||||
const placement = Standings.tournamentStandings(tournament).find(
|
||||
const standingsResult = Standings.tournamentStandings(tournament);
|
||||
const overallStandings = Standings.flattenStandings(standingsResult);
|
||||
const placement = overallStandings.find(
|
||||
(s) => s.team.id === data.tournamentTeamId,
|
||||
)?.placement;
|
||||
|
||||
|
|
@ -170,6 +172,15 @@ function StatSquares({
|
|||
{t("tournament:team.placement.footer")}
|
||||
</div>
|
||||
) : null}
|
||||
{standingsResult.type === "multi" ? (
|
||||
<div className="tournament__team__stat__sub">
|
||||
{
|
||||
standingsResult.standings.find((s) =>
|
||||
s.standings.some((s) => s.team.id === data.tournamentTeamId),
|
||||
)?.div
|
||||
}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
135
app/features/tournament/tournament-utils.test.ts
Normal file
135
app/features/tournament/tournament-utils.test.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import type { ParsedBracket } from "../tournament-bracket/core/Progression";
|
||||
import { getBracketProgressionLabel } from "./tournament-utils";
|
||||
|
||||
const createBracket = (name: string): ParsedBracket => ({
|
||||
name,
|
||||
type: "single_elimination",
|
||||
settings: {},
|
||||
requiresCheckIn: false,
|
||||
});
|
||||
|
||||
describe("getBracketProgressionLabel", () => {
|
||||
it("returns single bracket name when only one bracket is reachable", () => {
|
||||
const progression: ParsedBracket[] = [createBracket("Main Bracket")];
|
||||
|
||||
const result = getBracketProgressionLabel(0, progression);
|
||||
|
||||
expect(result).toBe("Main Bracket");
|
||||
});
|
||||
|
||||
it("returns common prefix when multiple brackets share a prefix", () => {
|
||||
const progression: ParsedBracket[] = [
|
||||
createBracket("Alpha"),
|
||||
createBracket("Alpha A"),
|
||||
createBracket("Alpha B"),
|
||||
];
|
||||
|
||||
progression[1].sources = [{ bracketIdx: 0, placements: [1] }];
|
||||
progression[2].sources = [{ bracketIdx: 0, placements: [2] }];
|
||||
|
||||
const result = getBracketProgressionLabel(0, progression);
|
||||
|
||||
expect(result).toBe("Alpha");
|
||||
});
|
||||
|
||||
it("trims whitespace from common prefix", () => {
|
||||
const progression: ParsedBracket[] = [
|
||||
createBracket("Playoff "),
|
||||
createBracket("Playoff Winner"),
|
||||
createBracket("Playoff Loser"),
|
||||
];
|
||||
|
||||
progression[1].sources = [{ bracketIdx: 0, placements: [1] }];
|
||||
progression[2].sources = [{ bracketIdx: 0, placements: [2] }];
|
||||
|
||||
const result = getBracketProgressionLabel(0, progression);
|
||||
|
||||
expect(result).toBe("Playoff");
|
||||
});
|
||||
|
||||
it("returns deepest bracket name when no common prefix exists", () => {
|
||||
const progression: ParsedBracket[] = [
|
||||
createBracket("Round Robin"),
|
||||
createBracket("Winner Bracket"),
|
||||
createBracket("Loser Bracket"),
|
||||
createBracket("Grand Finals"),
|
||||
];
|
||||
|
||||
progression[1].sources = [{ bracketIdx: 0, placements: [1] }];
|
||||
progression[2].sources = [{ bracketIdx: 0, placements: [2] }];
|
||||
progression[3].sources = [
|
||||
{ bracketIdx: 1, placements: [1] },
|
||||
{ bracketIdx: 2, placements: [1] },
|
||||
];
|
||||
|
||||
const result = getBracketProgressionLabel(0, progression);
|
||||
|
||||
expect(result).toBe("Grand Finals");
|
||||
});
|
||||
|
||||
it("handles single character prefix", () => {
|
||||
const progression: ParsedBracket[] = [
|
||||
createBracket("A"),
|
||||
createBracket("A1"),
|
||||
createBracket("A2"),
|
||||
];
|
||||
|
||||
progression[1].sources = [{ bracketIdx: 0, placements: [1] }];
|
||||
progression[2].sources = [{ bracketIdx: 0, placements: [2] }];
|
||||
|
||||
const result = getBracketProgressionLabel(0, progression);
|
||||
|
||||
expect(result).toBe("A");
|
||||
});
|
||||
|
||||
it("handles bracket progression with multiple levels", () => {
|
||||
const progression: ParsedBracket[] = [
|
||||
createBracket("Qualifier"),
|
||||
createBracket("Group A"),
|
||||
createBracket("Group B"),
|
||||
createBracket("Finals"),
|
||||
];
|
||||
|
||||
progression[1].sources = [{ bracketIdx: 0, placements: [1, 2] }];
|
||||
progression[2].sources = [{ bracketIdx: 0, placements: [3, 4] }];
|
||||
progression[3].sources = [
|
||||
{ bracketIdx: 1, placements: [1] },
|
||||
{ bracketIdx: 2, placements: [1] },
|
||||
];
|
||||
|
||||
const result = getBracketProgressionLabel(0, progression);
|
||||
|
||||
expect(result).toBe("Finals");
|
||||
});
|
||||
|
||||
it("returns bracket name for progression with partial common prefix", () => {
|
||||
const progression: ParsedBracket[] = [
|
||||
createBracket("Swiss"),
|
||||
createBracket("Swiss Upper"),
|
||||
createBracket("Swiss Lower"),
|
||||
];
|
||||
|
||||
progression[1].sources = [{ bracketIdx: 0, placements: [1, 2] }];
|
||||
progression[2].sources = [{ bracketIdx: 0, placements: [3, 4] }];
|
||||
|
||||
const result = getBracketProgressionLabel(0, progression);
|
||||
|
||||
expect(result).toBe("Swiss");
|
||||
});
|
||||
|
||||
it("handles empty string prefix by returning deepest bracket", () => {
|
||||
const progression: ParsedBracket[] = [
|
||||
createBracket("A"),
|
||||
createBracket("B"),
|
||||
createBracket("C"),
|
||||
];
|
||||
|
||||
progression[1].sources = [{ bracketIdx: 0, placements: [1] }];
|
||||
progression[2].sources = [{ bracketIdx: 1, placements: [1] }];
|
||||
|
||||
const result = getBracketProgressionLabel(0, progression);
|
||||
|
||||
expect(result).toBe("C");
|
||||
});
|
||||
});
|
||||
|
|
@ -9,6 +9,8 @@ import { assertUnreachable } from "../../utils/types";
|
|||
import { MapPool } from "../map-list-generator/core/map-pool";
|
||||
import * as Seasons from "../mmr/core/Seasons";
|
||||
import { BANNED_MAPS } from "../sendouq-settings/banned-maps";
|
||||
import type { ParsedBracket } from "../tournament-bracket/core/Progression";
|
||||
import * as Progression from "../tournament-bracket/core/Progression";
|
||||
import type { Tournament as TournamentClass } from "../tournament-bracket/core/Tournament";
|
||||
import type { TournamentData } from "../tournament-bracket/core/Tournament.server";
|
||||
import type { PlayedSet } from "./core/sets.server";
|
||||
|
|
@ -394,3 +396,45 @@ export function normalizedTeamCount({
|
|||
}) {
|
||||
return teamsCount * minMembersPerTeam;
|
||||
}
|
||||
|
||||
export function getBracketProgressionLabel(
|
||||
startingBracketIdx: number,
|
||||
progression: ParsedBracket[],
|
||||
): string {
|
||||
const reachableBracketIdxs = Progression.bracketsReachableFrom(
|
||||
startingBracketIdx,
|
||||
progression,
|
||||
);
|
||||
|
||||
const uniqueBracketIdxs = Array.from(new Set(reachableBracketIdxs));
|
||||
const bracketNames = uniqueBracketIdxs.map((idx) => progression[idx].name);
|
||||
|
||||
if (bracketNames.length === 1) {
|
||||
return bracketNames[0];
|
||||
}
|
||||
|
||||
let prefix = bracketNames[0];
|
||||
for (let i = 1; i < bracketNames.length; i++) {
|
||||
const name = bracketNames[i];
|
||||
let j = 0;
|
||||
while (j < prefix.length && j < name.length && prefix[j] === name[j]) {
|
||||
j++;
|
||||
}
|
||||
prefix = prefix.substring(0, j);
|
||||
if (prefix === "") break;
|
||||
}
|
||||
|
||||
prefix = prefix.trim();
|
||||
|
||||
if (!prefix) {
|
||||
const deepestBracketIdx = uniqueBracketIdxs.reduce((deepest, current) => {
|
||||
const currentDepth = Progression.bracketDepth(current, progression);
|
||||
const deepestDepth = Progression.bracketDepth(deepest, progression);
|
||||
return currentDepth > deepestDepth ? current : deepest;
|
||||
}, uniqueBracketIdxs[0]);
|
||||
|
||||
return progression[deepestBracketIdx].name;
|
||||
}
|
||||
|
||||
return prefix;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -483,6 +483,7 @@ export function findResultsByUserId(
|
|||
"CalendarEventResultTeam.placement",
|
||||
"CalendarEvent.participantCount",
|
||||
sql<Tables["TournamentResult"]["setResults"]>`null`.as("setResults"),
|
||||
sql<string | null>`null`.as("div"),
|
||||
sql<string | null>`null`.as("logoUrl"),
|
||||
"CalendarEvent.name as eventName",
|
||||
"CalendarEventResultTeam.id as teamId",
|
||||
|
|
@ -520,6 +521,7 @@ export function findResultsByUserId(
|
|||
"TournamentResult.placement",
|
||||
"TournamentResult.participantCount",
|
||||
"TournamentResult.setResults",
|
||||
"TournamentResult.div",
|
||||
eb
|
||||
.selectFrom("UserSubmittedImage")
|
||||
.select(["UserSubmittedImage.url"])
|
||||
|
|
|
|||
|
|
@ -124,6 +124,9 @@ export function UserResultsTable({
|
|||
>
|
||||
{result.eventName}
|
||||
</Link>
|
||||
{result.div ? (
|
||||
<span className="text-lighter">({result.div})</span>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -34,7 +34,9 @@ export default function UserResultsPage() {
|
|||
<div className="stack lg">
|
||||
<div className="stack horizontal justify-between items-center">
|
||||
<h2 className="text-lg">
|
||||
{showAll ? t("results.title") : t("results.highlights")}
|
||||
{showAll || !data.hasHighlightedResults
|
||||
? t("results.title")
|
||||
: t("results.highlights")}
|
||||
</h2>
|
||||
{user?.id === layoutData.user.id ? (
|
||||
<LinkButton
|
||||
|
|
|
|||
5
migrations/102-tournament-result-div.js
Normal file
5
migrations/102-tournament-result-div.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export function up(db) {
|
||||
db.transaction(() => {
|
||||
db.prepare(/* sql */ `alter table "TournamentResult" add "div" text`).run();
|
||||
})();
|
||||
}
|
||||
135
scripts/backfill-tournament-result-divisions.ts
Normal file
135
scripts/backfill-tournament-result-divisions.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import "dotenv/config";
|
||||
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 { allMatchResultsByTournamentId } from "../app/features/tournament-bracket/queries/allMatchResultsByTournamentId.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 = allMatchResultsByTournamentId(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);
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user