Add one tierOfOrdinal helper for the three places that look up a SendouQ tier

This commit is contained in:
Kalle
2026-11-10 19:59:20 +02:00
parent 628c84dec6
commit faeeb8b960
6 changed files with 53 additions and 31 deletions

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);
});
});

View File

@@ -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;
}

View File

@@ -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,
},
];

View File

@@ -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<ReturnType<SendouQClass["mapMatch"]>>;
export type SQGroupMember = NonNullable<SQGroup["members"]>[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({