mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-11-12 22:29:53 -06:00
Add one tierOfOrdinal helper for the three places that look up a SendouQ tier
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
22
app/features/mmr/mmr-utils.test.ts
Normal file
22
app/features/mmr/mmr-utils.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user