From faeeb8b960ec59ea626e30aa46a0d6c93a5c63d4 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Tue, 10 Nov 2026 19:59:20 +0200 Subject: [PATCH] Add one tierOfOrdinal helper for the three places that look up a SendouQ tier --- app/features/feed/core/FeedEvents.ts | 13 +++--------- app/features/mmr/mmr-constants.ts | 3 +++ app/features/mmr/mmr-utils.test.ts | 22 +++++++++++++++++++++ app/features/mmr/mmr-utils.ts | 18 ++++++++++++++++- app/features/mmr/tiered.server.ts | 6 ++---- app/features/sendouq/core/SendouQ.server.ts | 22 ++++++--------------- 6 files changed, 53 insertions(+), 31 deletions(-) create mode 100644 app/features/mmr/mmr-utils.test.ts diff --git a/app/features/feed/core/FeedEvents.ts b/app/features/feed/core/FeedEvents.ts index 868b581fd..f0b135ce4 100644 --- a/app/features/feed/core/FeedEvents.ts +++ b/app/features/feed/core/FeedEvents.ts @@ -1,4 +1,5 @@ import * as R from "remeda"; +import { tierOfOrdinal } from "~/features/mmr/mmr-utils"; import type { SkillTierInterval } from "~/features/mmr/tiered.server"; import { FEED_COMEBACK_MIN_BREAK_DAYS, @@ -162,8 +163,8 @@ export function derive({ const tiers = tiersOfSeason(play.season); if (play.isWin && play.previousOrdinal !== null && tiers) { - const from = tierOf(tiers.intervals, play.previousOrdinal); - const to = tierOf(tiers.intervals, play.ordinal); + const from = tierOfOrdinal(tiers.intervals, play.previousOrdinal); + const to = tierOfOrdinal(tiers.intervals, play.ordinal); const isNotable = !tiers.previousSeasonTier || tierRank(to) >= tierRank(tiers.previousSeasonTier); @@ -214,14 +215,6 @@ export function derive({ ]; } -function tierOf(intervals: SkillTierInterval[], ordinal: number): FeedTier { - const interval = intervals.find((t) => t.neededOrdinal! <= ordinal); - - return interval - ? { name: interval.name, isPlus: interval.isPlus } - : { name: "IRON", isPlus: false }; -} - /** Position on the ladder where each plus tier is its own step, e.g. Diamond+ is one above Diamond. */ export function tierRank(tier: { name: string; isPlus: boolean }) { return TIER_ORDER.indexOf(tier.name) * 2 + (tier.isPlus ? 1 : 0); diff --git a/app/features/mmr/mmr-constants.ts b/app/features/mmr/mmr-constants.ts index 05624eec4..fc6e3052d 100644 --- a/app/features/mmr/mmr-constants.ts +++ b/app/features/mmr/mmr-constants.ts @@ -64,6 +64,9 @@ export const TIERS_BEFORE_LEVIATHAN = [ export type TierName = (typeof TIERS)[number]["name"]; +/** Tier of an ordinal below every tier threshold. */ +export const LOWEST_TIER = { name: "IRON", isPlus: false } as const; + /** `Skill.season` of the dummy skill inserted to close a canceled match without a skill change. */ export const CANCELED_MATCH_SEASON = -1; diff --git a/app/features/mmr/mmr-utils.test.ts b/app/features/mmr/mmr-utils.test.ts new file mode 100644 index 000000000..185f9f9df --- /dev/null +++ b/app/features/mmr/mmr-utils.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, test } from "vitest"; +import { tierOfOrdinal } from "./mmr-utils"; +import type { SkillTierInterval } from "./tiered.server"; + +const INTERVALS: SkillTierInterval[] = [ + { name: "DIAMOND", isPlus: true, neededOrdinal: 30 }, + { name: "DIAMOND", isPlus: false, neededOrdinal: 20 }, + { name: "PLATINUM", isPlus: true }, +]; + +describe("tierOfOrdinal", () => { + test.each([ + { why: "above the top threshold", ordinal: 50, expected: "DIAMOND+" }, + { why: "exactly at a threshold", ordinal: 20, expected: "DIAMOND" }, + { why: "between two thresholds", ordinal: 25, expected: "DIAMOND" }, + { why: "under every threshold", ordinal: 10, expected: "IRON" }, + ])("$why", ({ ordinal, expected }) => { + const tier = tierOfOrdinal(INTERVALS, ordinal); + + expect(`${tier.name}${tier.isPlus ? "+" : ""}`).toBe(expected); + }); +}); diff --git a/app/features/mmr/mmr-utils.ts b/app/features/mmr/mmr-utils.ts index 4adcba9de..2675d6a22 100644 --- a/app/features/mmr/mmr-utils.ts +++ b/app/features/mmr/mmr-utils.ts @@ -7,7 +7,8 @@ import { } from "openskill"; import { invariant } from "~/utils/invariant"; import type { TierName } from "./mmr-constants"; -import { SP_BASE, SP_PER_ORDINAL, TIERS } from "./mmr-constants"; +import { LOWEST_TIER, SP_BASE, SP_PER_ORDINAL, TIERS } from "./mmr-constants"; +import type { SkillTierInterval, TieredSkill } from "./tiered.server"; const TAU = 0.3; @@ -95,3 +96,18 @@ export function compareTwoTiers(tier1: TierName, tier2: TierName) { TIERS.findIndex(({ name }) => name === tier2) ); } + +/** Tier the ordinal falls in, {@link LOWEST_TIER} when it is under every threshold (the lowest interval has none). */ +export function tierOfOrdinal( + intervals: SkillTierInterval[], + ordinalValue: number, +): TieredSkill["tier"] { + const interval = intervals.find( + (i) => + typeof i.neededOrdinal === "number" && ordinalValue >= i.neededOrdinal, + ); + + return interval + ? { name: interval.name, isPlus: interval.isPlus } + : LOWEST_TIER; +} diff --git a/app/features/mmr/tiered.server.ts b/app/features/mmr/tiered.server.ts index 006711dec..b6403cf16 100644 --- a/app/features/mmr/tiered.server.ts +++ b/app/features/mmr/tiered.server.ts @@ -11,6 +11,7 @@ import { type TierName, USER_LEADERBOARD_MIN_ENTRIES_FOR_LEVIATHAN, } from "./mmr-constants"; +import { tierOfOrdinal } from "./mmr-utils"; import * as SkillRepository from "./SkillRepository.server"; export interface TieredSkill { @@ -38,14 +39,11 @@ export async function freshUserSkills(season: number): Promise<{ isAccurateTiers, userSkills: Object.fromEntries( points.map((p) => { - const { name, isPlus } = intervals.find( - (t) => t.neededOrdinal! <= p.ordinal, - ) ?? { name: "IRON", isPlus: false }; return [ p.userId as number, { ordinal: p.ordinal, - tier: { name, isPlus }, + tier: tierOfOrdinal(intervals, p.ordinal), approximate: p.matchesCount < MATCHES_COUNT_NEEDED_FOR_LEADERBOARD, }, ]; diff --git a/app/features/sendouq/core/SendouQ.server.ts b/app/features/sendouq/core/SendouQ.server.ts index ef97725a4..7043525e6 100644 --- a/app/features/sendouq/core/SendouQ.server.ts +++ b/app/features/sendouq/core/SendouQ.server.ts @@ -6,7 +6,8 @@ import type { AuthenticatedUser } from "~/features/auth/core/user.server"; import * as ChatSystemMessage from "~/features/chat/ChatSystemMessage.server"; import * as MapVotes from "~/features/match-profile/core/MapVotes"; import * as Seasons from "~/features/mmr/core/Seasons"; -import { defaultOrdinal } from "~/features/mmr/mmr-utils"; +import { LOWEST_TIER } from "~/features/mmr/mmr-constants"; +import { defaultOrdinal, tierOfOrdinal } from "~/features/mmr/mmr-utils"; import { type TieredSkill, userSkills } from "~/features/mmr/tiered.server"; import * as SQGroupRepository from "~/features/sendouq/SQGroupRepository.server"; import * as SendouQMatch from "~/features/sendouq-match/core/SendouQMatch"; @@ -52,7 +53,6 @@ export type SQOwnGroup = SerializeFrom< export type SQMatch = SerializeFrom>; export type SQGroupMember = NonNullable[number]; -const FALLBACK_TIER = { isPlus: false, name: "IRON" } as const; const SECONDS_TILL_STALE = process.env.NODE_ENV === "development" || IS_E2E_TEST_RUN ? 1_000_000 : 1_800; @@ -325,8 +325,8 @@ class SendouQClass { } const tierRangeOrExact = tierDifferenceToRangeOrExact({ - ourTier: ownTier ?? FALLBACK_TIER, - theirTier: group.tier ?? FALLBACK_TIER, + ourTier: ownTier ?? LOWEST_TIER, + theirTier: group.tier ?? LOWEST_TIER, isAccurateTiers: this.#isAccurateTiers, }); @@ -522,11 +522,7 @@ class SendouQClass { ); } - #groupTier( - group: DBGroupRow | DBMatch["groupAlpha"] | DBMatch["groupBravo"], - ): TieredSkill["tier"] | undefined { - if (!group.members) return; - + #groupTier(group: DBGroupRow): TieredSkill["tier"] { const skills = group.members.map( (m) => this.#userSkills[String(m.id)] ?? { ordinal: defaultOrdinal() }, ); @@ -534,13 +530,7 @@ class SendouQClass { const averageOrdinal = skills.reduce((acc, s) => acc + s.ordinal, 0) / skills.length; - return ( - this.#intervals.find( - (i) => - typeof i.neededOrdinal === "number" && - averageOrdinal >= i.neededOrdinal, - ) ?? { isPlus: false, name: "IRON" } - ); + return tierOfOrdinal(this.#intervals, averageOrdinal); } #isSuitableLookingGroup({