SendouQ group skill ranges

This commit is contained in:
Kalle
2024-06-24 17:51:37 +03:00
parent b914393bd8
commit f5bf17de96
6 changed files with 218 additions and 27 deletions

View File

@@ -158,6 +158,18 @@ export function GroupCard({
{group.tier.isPlus ? "+" : ""}
</div>
) : null}
{group.tierRange ? (
<div className="stack sm items-center">
<div className="q__group__tier-diff-text">
±{group.tierRange.diff}
</div>
<div className="stack sm horizontal items-center text-lg">
<TierImage tier={group.tierRange.range[0]} width={38} />
—
<TierImage tier={group.tierRange.range[1]} width={38} />
</div>
</div>
) : null}
{group.skillDifference ? (
<GroupSkillDifference skillDifference={group.skillDifference} />
) : null}

View File

@@ -0,0 +1,92 @@
import { suite } from "uvu";
import * as assert from "uvu/assert";
import { tierDifferenceToRangeOrExact } from "./groups.server";
const paramsToExpected = new Map<
[
Parameters<typeof tierDifferenceToRangeOrExact>[0]["ourTier"],
Parameters<typeof tierDifferenceToRangeOrExact>[0]["theirTier"],
],
ReturnType<typeof tierDifferenceToRangeOrExact>["tier"]
>()
// exact
.set(
[
{ isPlus: false, name: "GOLD" },
{ isPlus: false, name: "GOLD" },
],
{ isPlus: false, name: "GOLD" },
)
// 1 place difference
.set(
[
{ isPlus: false, name: "GOLD" },
{ isPlus: true, name: "GOLD" },
],
[
{ isPlus: true, name: "SILVER" },
{ isPlus: true, name: "GOLD" },
],
)
// 2 places difference
.set(
[
{ isPlus: false, name: "GOLD" },
{ isPlus: false, name: "PLATINUM" },
],
[
{ isPlus: false, name: "SILVER" },
{ isPlus: false, name: "PLATINUM" },
],
)
// too high, has to be exact
.set(
[
{ isPlus: true, name: "LEVIATHAN" },
{ isPlus: false, name: "LEVIATHAN" },
],
{ isPlus: false, name: "LEVIATHAN" },
)
// too low, has to be exact
.set(
[
{ isPlus: false, name: "IRON" },
{ isPlus: true, name: "IRON" },
],
{ isPlus: true, name: "IRON" },
)
// not max rank but still too high
.set(
[
{ isPlus: false, name: "LEVIATHAN" },
{ isPlus: false, name: "DIAMOND" },
],
{ isPlus: false, name: "DIAMOND" },
);
const TierDifferenceToRangeOrExact = suite("tierDifferenceToRangeOrExact()");
for (const [input, expected] of paramsToExpected) {
TierDifferenceToRangeOrExact(
`works for ${JSON.stringify(input)} -> ${JSON.stringify(expected)}`,
() => {
const result = tierDifferenceToRangeOrExact({
ourTier: input[0],
theirTier: input[1],
hasLeviathan: true,
}).tier;
assert.equal(result, expected);
},
);
}
TierDifferenceToRangeOrExact("works before leviathan", () => {
const result = tierDifferenceToRangeOrExact({
ourTier: { isPlus: true, name: "DIAMOND" },
theirTier: { isPlus: false, name: "DIAMOND" },
hasLeviathan: false,
}).tier;
assert.equal(result, { isPlus: false, name: "DIAMOND" });
});
TierDifferenceToRangeOrExact.run();

View File

@@ -1,3 +1,4 @@
import clone from "just-clone";
import type { Tables } from "~/db/tables";
import type { Group } from "~/db/types";
import { TIERS } from "~/features/mmr/mmr-constants";
@@ -304,21 +305,6 @@ export function sortGroupsBySkillAndSentiment({
return {
...groups,
neutral: groups.neutral.sort((a, b) => {
const aTier =
a.tier?.name ??
resolveGroupSkill({
group: a as LookingGroupWithInviteCode,
userSkills,
intervals,
})?.name;
const bTier =
b.tier?.name ??
resolveGroupSkill({
group: b as LookingGroupWithInviteCode,
userSkills,
intervals,
})?.name;
const aSentiment = groupSentiment(a);
const bSentiment = groupSentiment(b);
@@ -329,6 +315,16 @@ export function sortGroupsBySkillAndSentiment({
if (bSentiment === "POSITIVE") return 1;
}
const aDiff = a.tierRange?.diff ?? 0;
const bDiff = b.tierRange?.diff ?? 0;
if (aDiff || bDiff) {
return aDiff - bDiff;
}
const aTier = a.tier?.name;
const bTier = b.tier?.name;
const aTierDiff = tierDiff(aTier);
const bTierDiff = tierDiff(bTier);
@@ -375,6 +371,36 @@ export function addSkillsToGroups({
};
}
const FALLBACK_TIER = { isPlus: false, name: "IRON" } as const;
export function addSkillRangeToGroups({
groups,
hasLeviathan,
}: { groups: DividedGroups; hasLeviathan: boolean }) {
const addRange = (group: LookingGroup) => {
if (group.members && group.members.length !== FULL_GROUP_SIZE) return group;
const range = tierDifferenceToRangeOrExact({
ourTier: groups.own?.tier ?? FALLBACK_TIER,
theirTier: group.tier ?? FALLBACK_TIER,
hasLeviathan,
});
if (!Array.isArray(range.tier)) return group;
return {
...group,
tierRange: { range: range.tier, diff: range.diff },
tier: undefined,
};
};
return {
own: groups.own,
neutral: groups.neutral.map(addRange),
likesReceived: groups.likesReceived.map(addRange),
};
}
export function membersNeededForFull(currentSize: number) {
return FULL_GROUP_SIZE - currentSize;
}
@@ -395,14 +421,11 @@ function resolveGroupSkill({
const averageOrdinal =
skills.reduce((acc, s) => acc + s.ordinal, 0) / skills.length;
const tier = intervals.find(
(i) => i.neededOrdinal && averageOrdinal > i.neededOrdinal,
) ?? { isPlus: false, name: "IRON" };
// For Leviathan we don't specify if it's plus or not
return tier.name === "LEVIATHAN"
? { name: "LEVIATHAN", isPlus: false }
: { name: tier.name, isPlus: tier.isPlus };
return (
intervals.find(
(i) => i.neededOrdinal && averageOrdinal > i.neededOrdinal,
) ?? { isPlus: false, name: "IRON" }
);
}
export function groupExpiryStatus(
@@ -428,3 +451,51 @@ export function groupExpiryStatus(
return null;
}
const allTiersOrdered = TIERS.flatMap((tier) => [
{ name: tier.name, isPlus: true },
{ name: tier.name, isPlus: false },
]).reverse();
export function tierDifferenceToRangeOrExact({
ourTier,
theirTier,
hasLeviathan,
}: {
ourTier: TieredSkill["tier"];
theirTier: TieredSkill["tier"];
hasLeviathan: boolean;
}): {
diff: number;
tier: TieredSkill["tier"] | [TieredSkill["tier"], TieredSkill["tier"]];
} {
if (ourTier.name === theirTier.name && ourTier.isPlus === theirTier.isPlus) {
return { diff: 0, tier: clone(ourTier) };
}
const tiers = hasLeviathan
? allTiersOrdered
: allTiersOrdered.filter((tier) => tier.name !== "LEVIATHAN");
const tier1Idx = tiers.findIndex(
(t) => t.name === ourTier.name && t.isPlus === ourTier.isPlus,
);
const tier2Idx = tiers.findIndex(
(t) => t.name === theirTier.name && t.isPlus === theirTier.isPlus,
);
invariant(tier1Idx !== -1, "tier1 not found");
invariant(tier2Idx !== -1, "tier2 not found");
const idxDiff = Math.abs(tier1Idx - tier2Idx);
const lowerBound = tier1Idx - idxDiff;
const upperBound = tier1Idx + idxDiff;
if (lowerBound < 0 || upperBound >= tiers.length) {
return { diff: idxDiff, tier: clone(theirTier) };
}
const lowerTier = tiers[lowerBound];
const upperTier = tiers[upperBound];
return { diff: idxDiff, tier: [clone(lowerTier), clone(upperTier)] };
}

View File

@@ -14,6 +14,10 @@ export type LookingGroup = {
id: number;
createdAt: Group["createdAt"];
tier?: TieredSkill["tier"];
tierRange?: {
range: [TieredSkill["tier"], TieredSkill["tier"]];
diff: number;
};
isReplay?: boolean;
isNoScreen?: boolean;
isLiked?: boolean;

View File

@@ -318,6 +318,10 @@
transform: translate(-50%, -50%);
}
.q__group__tier-diff-text {
font-size: 3rem;
}
.q__member-adder__input {
--input-width: 11rem;
width: 11rem;

View File

@@ -52,6 +52,7 @@ import {
addFutureMatchModes,
addNoScreenIndicator,
addReplayIndicator,
addSkillRangeToGroups,
addSkillsToGroups,
censorGroups,
divideGroups,
@@ -455,9 +456,11 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
const season = currentOrPreviousSeason(new Date());
const { intervals, userSkills: calculatedUserSkills } = userSkills(
season!.nth,
);
const {
intervals,
userSkills: calculatedUserSkills,
isAccurateTiers,
} = userSkills(season!.nth);
const groupsWithSkills = addSkillsToGroups({
groups: dividedGroups,
intervals,
@@ -485,8 +488,13 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
: false,
});
const sortedGroups = sortGroupsBySkillAndSentiment({
const rangedGroups = addSkillRangeToGroups({
groups: censoredGroups,
hasLeviathan: isAccurateTiers,
});
const sortedGroups = sortGroupsBySkillAndSentiment({
groups: rangedGroups,
intervals,
userSkills: calculatedUserSkills,
userId: user?.id,