Skip to content

Commit

Permalink
Seeding points (#1969)
Browse files Browse the repository at this point in the history
* Initial

* Seed page initial

* Progress

* tests

* Fix e2e tests
  • Loading branch information
Sendouc authored Nov 23, 2024
1 parent e59b271 commit feebdfa
Show file tree
Hide file tree
Showing 18 changed files with 549 additions and 474 deletions.
12 changes: 10 additions & 2 deletions app/db/tables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,8 +366,8 @@ export interface Skill {
matchesCount: number;
mu: number;
ordinal: number;
season: number;
sigma: number;
season: number;
tournamentId: number | null;
userId: number | null;
}
Expand All @@ -377,6 +377,14 @@ export interface SkillTeamUser {
userId: number;
}

export interface SeedingSkill {
mu: number;
ordinal: number;
sigma: number;
userId: number;
type: "RANKED" | "UNRANKED";
}

export interface SplatoonPlayer {
id: GeneratedAlways<number>;
splId: string;
Expand Down Expand Up @@ -878,7 +886,6 @@ export interface DB {
LFGPost: LFGPost;
MapPoolMap: MapPoolMap;
MapResult: MapResult;
migrations: Migrations;
PlayerResult: PlayerResult;
PlusSuggestion: PlusSuggestion;
PlusTier: PlusTier;
Expand All @@ -887,6 +894,7 @@ export interface DB {
ReportedWeapon: ReportedWeapon;
Skill: Skill;
SkillTeamUser: SkillTeamUser;
SeedingSkill: SeedingSkill;
SplatoonPlayer: SplatoonPlayer;
TaggedArt: TaggedArt;
Team: Team;
Expand Down
13 changes: 13 additions & 0 deletions app/features/mmr/mmr-utils.server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { rating } from "openskill";
import type { Tables } from "../../db/tables";
import { identifierToUserIds } from "./mmr-utils";
import { findCurrentSkillByUserId } from "./queries/findCurrentSkillByUserId.server";
import { findCurrentTeamSkillByIdentifier } from "./queries/findCurrentTeamSkillByIdentifier.server";
import { findSeedingSkill } from "./queries/findSeedingSkill.server";

export function queryCurrentUserRating({
userId,
Expand All @@ -19,6 +21,17 @@ export function queryCurrentUserRating({
return { rating: rating(skill), matchesCount: skill.matchesCount };
}

export function queryCurrentUserSeedingRating(args: {
userId: number;
type: Tables["SeedingSkill"]["type"];
}) {
const skill = findSeedingSkill(args);

if (!skill) return rating();

return skill;
}

export function queryCurrentTeamRating({
identifier,
season,
Expand Down
21 changes: 21 additions & 0 deletions app/features/mmr/queries/findSeedingSkill.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { sql } from "~/db/sql";
import type { Tables } from "../../../db/tables";

const stm = sql.prepare(/* sql */ `
select
"mu",
"sigma"
from
"SeedingSkill"
where
"userId" = @userId
and
"type" = @type
`);

export function findSeedingSkill(args: {
userId: number;
type: Tables["SeedingSkill"]["type"];
}) {
return stm.get(args) as Pick<Tables["SeedingSkill"], "mu" | "sigma"> | null;
}
40 changes: 24 additions & 16 deletions app/features/tournament-bracket/core/Tournament.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,22 +81,16 @@ export class Tournament {
a: TournamentData["ctx"]["teams"][number],
b: TournamentData["ctx"]["teams"][number],
) {
const aPlus = a.members
.flatMap((a) => (a.plusTier ? [a.plusTier] : []))
.sort((a, b) => a - b)
.slice(0, 4);
const bPlus = b.members
.flatMap((b) => (b.plusTier ? [b.plusTier] : []))
.sort((a, b) => a - b)
.slice(0, 4);

for (let i = 0; i < 4; i++) {
if (aPlus[i] && !bPlus[i]) return -1;
if (!aPlus[i] && bPlus[i]) return 1;

if (aPlus[i] !== bPlus[i]) {
return aPlus[i] - bPlus[i];
}
if (a.avgSeedingSkillOrdinal && b.avgSeedingSkillOrdinal) {
return b.avgSeedingSkillOrdinal - a.avgSeedingSkillOrdinal;
}

if (a.avgSeedingSkillOrdinal && !b.avgSeedingSkillOrdinal) {
return -1;
}

if (!a.avgSeedingSkillOrdinal && b.avgSeedingSkillOrdinal) {
return 1;
}

return a.createdAt - b.createdAt;
Expand Down Expand Up @@ -528,6 +522,20 @@ export class Tournament {
});
}

/** What seeding skill rating this tournament counts for */
get skillCountsFor() {
if (this.ranked) {
return "RANKED";
}

// exclude gimmicky tournaments
if (this.minMembersPerTeam === 4 && !this.ctx.tags?.includes("SPECIAL")) {
return "UNRANKED";
}

return null;
}

get minMembersPerTeam() {
return this.ctx.settings.minMembersPerTeam ?? 4;
}
Expand Down
22 changes: 20 additions & 2 deletions app/features/tournament-bracket/core/summarizer.server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import shuffle from "just-shuffle";
import type { Rating } from "node_modules/openskill/dist/types";
import { ordinal } from "openskill";
import type {
MapResult,
PlayerResult,
Expand All @@ -13,6 +14,7 @@ import {
} from "~/features/mmr/mmr-utils";
import { removeDuplicates } from "~/utils/arrays";
import invariant from "~/utils/invariant";
import type { Tables } from "../../../db/tables";
import type { AllMatchResult } from "../queries/allMatchResultsByTournamentId.server";
import type { Standing } from "./Bracket";

Expand All @@ -21,6 +23,7 @@ export interface TournamentSummary {
Skill,
"tournamentId" | "id" | "ordinal" | "season" | "groupMatchId"
>[];
seedingSkills: Tables["SeedingSkill"][];
mapResultDeltas: Omit<MapResult, "season">[];
playerResultDeltas: Omit<PlayerResult, "season">[];
tournamentResults: Omit<TournamentResult, "tournamentId" | "isHighlight">[];
Expand All @@ -40,6 +43,8 @@ export function tournamentSummary({
queryCurrentTeamRating,
queryTeamPlayerRatingAverage,
queryCurrentUserRating,
queryCurrentSeedingRating,
seedingSkillCountsFor,
calculateSeasonalStats = true,
}: {
results: AllMatchResult[];
Expand All @@ -48,6 +53,8 @@ export function tournamentSummary({
queryCurrentTeamRating: (identifier: string) => Rating;
queryTeamPlayerRatingAverage: (identifier: string) => Rating;
queryCurrentUserRating: (userId: number) => Rating;
queryCurrentSeedingRating: (userId: number) => Rating;
seedingSkillCountsFor: Tables["SeedingSkill"]["type"] | null;
calculateSeasonalStats?: boolean;
}): TournamentSummary {
const userIdsToTeamId = userIdsToTeamIdRecord(teams);
Expand All @@ -62,6 +69,17 @@ export function tournamentSummary({
queryTeamPlayerRatingAverage,
})
: [],
seedingSkills: seedingSkillCountsFor
? calculateIndividualPlayerSkills({
queryCurrentUserRating: queryCurrentSeedingRating,
results,
userIdsToTeamId,
}).map((skill) => ({
...skill,
type: seedingSkillCountsFor,
ordinal: ordinal(skill),
}))
: [],
mapResultDeltas: calculateSeasonalStats
? mapResultDeltas({ results, userIdsToTeamId })
: [],
Expand All @@ -75,7 +93,7 @@ export function tournamentSummary({
};
}

function userIdsToTeamIdRecord(teams: TeamsArg) {
export function userIdsToTeamIdRecord(teams: TeamsArg) {
const result: UserIdToTeamId = {};

for (const team of teams) {
Expand All @@ -102,7 +120,7 @@ function skills(args: {
return result;
}

function calculateIndividualPlayerSkills({
export function calculateIndividualPlayerSkills({
results,
userIdsToTeamId,
queryCurrentUserRating,
Expand Down
37 changes: 36 additions & 1 deletion app/features/tournament-bracket/core/summarizer.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ordinal, rating } from "openskill";
import { describe, expect, test } from "vitest";
import invariant from "~/utils/invariant";
import type { Tables } from "../../../db/tables";
import type { AllMatchResult } from "../queries/allMatchResultsByTournamentId.server";
import type { TournamentDataTeam } from "./Tournament.server";
import { tournamentSummary } from "./summarizer.server";
Expand All @@ -14,6 +15,7 @@ describe("tournamentSummary()", () => {
createdAt: 0,
id: teamId,
inviteCode: null,
avgSeedingSkillOrdinal: null,
mapPool: [],
members: userIds.map((userId) => ({
country: null,
Expand All @@ -38,7 +40,13 @@ describe("tournamentSummary()", () => {
pickupAvatarUrl: null,
});

function summarize({ results }: { results?: AllMatchResult[] } = {}) {
function summarize({
results,
seedingSkillCountsFor,
}: {
results?: AllMatchResult[];
seedingSkillCountsFor?: Tables["SeedingSkill"]["type"];
} = {}) {
return tournamentSummary({
finalStandings: [
{
Expand Down Expand Up @@ -123,6 +131,8 @@ describe("tournamentSummary()", () => {
queryCurrentTeamRating: () => rating(),
queryCurrentUserRating: () => rating(),
queryTeamPlayerRatingAverage: () => rating(),
queryCurrentSeedingRating: () => rating(),
seedingSkillCountsFor: seedingSkillCountsFor ?? null,
});
}

Expand All @@ -141,6 +151,31 @@ describe("tournamentSummary()", () => {
expect(ordinal(winnerSkill)).toBeGreaterThan(ordinal(loserSkill));
});

test("seeding skill is calculated the same as normal skill", () => {
const summary = summarize({ seedingSkillCountsFor: "RANKED" });
const winnerSkill = summary.skills.find((s) => s.userId === 1);
const winnerSeedingSkill = summary.skills.find((s) => s.userId === 1);

invariant(winnerSkill, "winnerSkill should be defined");
invariant(winnerSeedingSkill, "winnerSeedingSkill should be defined");

expect(ordinal(winnerSkill)).toBe(ordinal(winnerSeedingSkill));
});

test("no seeding skill calculated if seedingSkillCountsFor is null", () => {
const summary = summarize();

expect(summary.seedingSkills.length).toBe(0);
});

test("seeding skills type matches the given seedingSkillCountsFor", () => {
const summary = summarize({ seedingSkillCountsFor: "RANKED" });
expect(summary.seedingSkills[0].type).toBe("RANKED");

const summary2 = summarize({ seedingSkillCountsFor: "UNRANKED" });
expect(summary2.seedingSkills[0].type).toBe("UNRANKED");
});

const resultsWith20: AllMatchResult[] = [
{
maps: [
Expand Down
Loading

0 comments on commit feebdfa

Please sign in to comment.