Add friend code & seeding power to public tournament teams API Closes #2065

This commit is contained in:
Kalle 2025-02-01 10:53:01 +02:00
parent d035873a09
commit daf7b3fef7
4 changed files with 63 additions and 6 deletions

View File

@ -3,7 +3,10 @@ import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/sqlite";
import { cors } from "remix-utils/cors";
import { z } from "zod";
import { db } from "~/db/sql";
import { ordinalToSp } from "~/features/mmr/mmr-utils";
import * as TournamentRepository from "~/features/tournament/TournamentRepository.server";
import i18next from "~/modules/i18n/i18next.server";
import { nullifyingAvg } from "~/utils/arrays";
import { databaseTimestampToDate } from "~/utils/dates";
import { parseParams } from "~/utils/remix.server";
import { userSubmittedImage } from "~/utils/urls";
@ -66,6 +69,16 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
eb
.selectFrom("TournamentTeamMember")
.innerJoin("User", "User.id", "TournamentTeamMember.userId")
.leftJoin("SeedingSkill as RankedSeedingSkill", (join) =>
join
.onRef("User.id", "=", "RankedSeedingSkill.userId")
.on("RankedSeedingSkill.type", "=", "RANKED"),
)
.leftJoin("SeedingSkill as UnrankedSeedingSkill", (join) =>
join
.onRef("User.id", "=", "UnrankedSeedingSkill.userId")
.on("UnrankedSeedingSkill.type", "=", "UNRANKED"),
)
.select([
"User.id as userId",
"User.username",
@ -75,6 +88,8 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
"TournamentTeamMember.inGameName",
"TournamentTeamMember.isOwner",
"TournamentTeamMember.createdAt",
"RankedSeedingSkill.ordinal as rankedOrdinal",
"UnrankedSeedingSkill.ordinal as unrankedOrdinal",
])
.whereRef(
"TournamentTeamMember.tournamentTeamId",
@ -94,6 +109,8 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
.orderBy("TournamentTeam.createdAt asc")
.execute();
const friendCodes = await TournamentRepository.friendCodesByTournamentId(id);
const logoUrl = (team: (typeof teams)[number]) => {
const url = team.team?.logoUrl ?? team.avatarUrl;
if (!url) return null;
@ -113,6 +130,14 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
seed: team.seed,
registeredAt: databaseTimestampToDate(team.createdAt).toISOString(),
checkedIn: Boolean(team.checkedInAt),
seedingPower: {
ranked: toSeedingPowerSP(
team.members.map((member) => member.rankedOrdinal),
),
unranked: toSeedingPowerSP(
team.members.map((member) => member.unrankedOrdinal),
),
},
members: team.members.map((member) => {
return {
userId: member.userId,
@ -124,6 +149,7 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
: null,
captain: Boolean(member.isOwner),
inGameName: member.inGameName,
friendCode: friendCodes[member.userId],
joinedAt: databaseTimestampToDate(member.createdAt).toISOString(),
};
}),
@ -145,3 +171,13 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
return await cors(request, json(result));
};
function toSeedingPowerSP(ordinals: (number | null)[]) {
const avg = nullifyingAvg(
ordinals.filter((ordinal) => typeof ordinal === "number"),
);
if (typeof avg !== "number") return null;
return ordinalToSp(avg);
}

View File

@ -115,6 +115,15 @@ export type GetTournamentTeamsResponse = Array<{
logoUrl: string | null;
seed: number | null;
mapPool: Array<StageWithMode> | null;
/**
* Seeding power is a non-resetting MMR value that is used for sendou.ink's autoseeding capabilities.
* It is calculated as the average of the team's members' seeding power.
* Ranked and unranked tournaments contribute to different seeding power values.
*/
seedingPower: {
ranked: number | null;
unranked: number | null;
};
members: Array<{
userId: number;
/**
@ -141,6 +150,12 @@ export type GetTournamentTeamsResponse = Array<{
* @example "Sendou#2955"
*/
inGameName: string | null;
/**
* Switch friend code used for identification purposes.
*
* @example "1234-5678-9101"
*/
friendCode: string;
/**
* @example "2024-01-12T20:00:00.000Z"
*/

View File

@ -12,7 +12,7 @@ import type {
import * as Progression from "~/features/tournament-bracket/core/Progression";
import { Status } from "~/modules/brackets-model";
import { modesShort } from "~/modules/in-game-lists";
import { nullFilledArray } from "~/utils/arrays";
import { nullFilledArray, nullifyingAvg } from "~/utils/arrays";
import { databaseTimestampNow, dateToDatabaseTimestamp } from "~/utils/dates";
import { COMMON_USER_FIELDS, userChatNameColor } from "~/utils/kysely.server";
import type { Unwrapped } from "~/utils/types";
@ -317,11 +317,6 @@ export async function findById(id: number) {
};
}
function nullifyingAvg(values: number[]) {
if (values.length === 0) return null;
return values.reduce((acc, cur) => acc + cur, 0) / values.length;
}
export async function findChildTournaments(parentTournamentId: number) {
const rows = await db
.selectFrom("Tournament")

View File

@ -97,3 +97,14 @@ export function pickRandomItem<T>(array: T[]): T {
export function filterOutFalsy<T>(arr: (T | null | undefined)[]): T[] {
return arr.filter(Boolean) as T[];
}
/**
* Calculates the average of an array of numbers. If the array is empty, returns null.
*
* @param values - An array of numbers to calculate the average of.
* @returns The average of the numbers in the array, or null if the array is empty.
*/
export function nullifyingAvg(values: number[]) {
if (values.length === 0) return null;
return values.reduce((acc, cur) => acc + cur, 0) / values.length;
}