mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-27 21:58:08 -05:00
SendouQ group skill ranges
This commit is contained in:
@@ -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}
|
||||
|
||||
92
app/features/sendouq/core/groups.server.test.ts
Normal file
92
app/features/sendouq/core/groups.server.test.ts
Normal 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();
|
||||
@@ -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)] };
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user