From 4f8f45fc9eaa0d42d92c04d82c4cedf34d959d69 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Mon, 28 Feb 2022 20:27:42 +0200 Subject: [PATCH] Show relative team MMR for ranked teams --- app/components/icons/ArrowDown.tsx | 18 +++++++ app/components/icons/ArrowUp.tsx | 18 +++++++ app/components/icons/DoubleArrowDown.tsx | 16 +++++++ app/components/icons/DoubleArrowUp.tsx | 18 +++++++ app/components/icons/Minus.tsx | 18 +++++++ app/components/play/GroupCard.tsx | 54 +++++++++++++++++++++ app/constants.ts | 3 ++ app/core/play/utils.test.ts | 26 ++++++++++ app/core/play/utils.ts | 60 ++++++++++++++++++++++-- app/routes/play/looking.tsx | 6 ++- app/styles/play-layout.css | 13 +++++ 11 files changed, 244 insertions(+), 6 deletions(-) create mode 100644 app/components/icons/ArrowDown.tsx create mode 100644 app/components/icons/ArrowUp.tsx create mode 100644 app/components/icons/DoubleArrowDown.tsx create mode 100644 app/components/icons/DoubleArrowUp.tsx create mode 100644 app/components/icons/Minus.tsx diff --git a/app/components/icons/ArrowDown.tsx b/app/components/icons/ArrowDown.tsx new file mode 100644 index 000000000..966d2b48a --- /dev/null +++ b/app/components/icons/ArrowDown.tsx @@ -0,0 +1,18 @@ +export function ArrowDownIcon({ className }: { className?: string }) { + return ( + + + + ); +} diff --git a/app/components/icons/ArrowUp.tsx b/app/components/icons/ArrowUp.tsx new file mode 100644 index 000000000..7ce0639ff --- /dev/null +++ b/app/components/icons/ArrowUp.tsx @@ -0,0 +1,18 @@ +export function ArrowUpIcon({ className }: { className?: string }) { + return ( + + + + ); +} diff --git a/app/components/icons/DoubleArrowDown.tsx b/app/components/icons/DoubleArrowDown.tsx new file mode 100644 index 000000000..1494cbf7e --- /dev/null +++ b/app/components/icons/DoubleArrowDown.tsx @@ -0,0 +1,16 @@ +export function DoubleArrowDownIcon({ className }: { className?: string }) { + return ( + + + + ); +} diff --git a/app/components/icons/DoubleArrowUp.tsx b/app/components/icons/DoubleArrowUp.tsx new file mode 100644 index 000000000..203ba0074 --- /dev/null +++ b/app/components/icons/DoubleArrowUp.tsx @@ -0,0 +1,18 @@ +export function DoubleArrowUpIcon({ className }: { className?: string }) { + return ( + + + + ); +} diff --git a/app/components/icons/Minus.tsx b/app/components/icons/Minus.tsx new file mode 100644 index 000000000..f3ce9335e --- /dev/null +++ b/app/components/icons/Minus.tsx @@ -0,0 +1,18 @@ +export function MinusIcon({ className }: { className?: string }) { + return ( + + + + ); +} diff --git a/app/components/play/GroupCard.tsx b/app/components/play/GroupCard.tsx index d4903a415..417b533c5 100644 --- a/app/components/play/GroupCard.tsx +++ b/app/components/play/GroupCard.tsx @@ -5,6 +5,11 @@ import type { LookingActionSchema, LookingLoaderDataGroup, } from "~/routes/play/looking"; +import { ArrowDownIcon } from "../icons/ArrowDown"; +import { ArrowUpIcon } from "../icons/ArrowUp"; +import { DoubleArrowDownIcon } from "../icons/DoubleArrowDown"; +import { DoubleArrowUpIcon } from "../icons/DoubleArrowUp"; +import { MinusIcon } from "../icons/Minus"; import { GroupMembers } from "./GroupMembers"; export function GroupCard({ @@ -69,6 +74,7 @@ export function GroupCard({ {group.teamMMR.value} )} + {group.MMRRelation && } {action === "UNITE_GROUPS" && ( ); } + +function MMRRelation({ + relation, +}: { + relation: NonNullable; +}) { + switch (relation) { + case "CLOSE": { + return ( +
+ Close SP +
+ ); + } + case "BIT_HIGHER": { + return ( +
+ A bit higher SP +
+ ); + } + case "BIT_LOWER": { + return ( +
+ A bit lower SP +
+ ); + } + case "HIGHER": { + return ( +
+ Higher SP +
+ ); + } + case "LOWER": { + return ( +
+ Lower SP +
+ ); + } + default: { + const exhaustive: never = relation; + throw new Error(`Unknown relation: ${JSON.stringify(exhaustive)}`); + } + } +} diff --git a/app/constants.ts b/app/constants.ts index 76be1193c..f530891a5 100644 --- a/app/constants.ts +++ b/app/constants.ts @@ -23,6 +23,9 @@ export const MMR_TOPX_VISIBILITY_CUTOFF = 50; export const LFG_AMOUNT_OF_STAGES_TO_GENERATE = 7; // export const LFG_AMOUNT_OF_STAGES_TO_GENERATE = 9; +export const CLOSE_MMR_LIMIT = 250; +export const BIT_HIGHER_MMR_LIMIT = 750; + export const checkInClosesDate = (startTime: string): Date => { return new Date(new Date(startTime).getTime() - 1000 * 10); }; diff --git a/app/core/play/utils.test.ts b/app/core/play/utils.test.ts index 31e962309..86731681b 100644 --- a/app/core/play/utils.test.ts +++ b/app/core/play/utils.test.ts @@ -1,6 +1,8 @@ import { suite } from "uvu"; import * as assert from "uvu/assert"; +import { BIT_HIGHER_MMR_LIMIT } from "~/constants"; import { + calculateDifference, groupsToWinningAndLosingPlayerIds, scoresAreIdentical, uniteGroupInfo, @@ -12,6 +14,7 @@ const ScoresAreIdentical = suite("scoresAreIdentical()"); const GroupsToWinningAndLosingPlayerIds = suite( "groupsToWinningAndLosingPlayerIds()" ); +const CalculateDifference = suite("calculateDifference()"); const SMALL_GROUP: UniteGroupInfoArg = { id: "small", memberCount: 1 }; const BIG_GROUP: UniteGroupInfoArg = { id: "big", memberCount: 3 }; @@ -96,6 +99,29 @@ GroupsToWinningAndLosingPlayerIds( } ); +CalculateDifference("Close", () => { + assert.equal(calculateDifference({ ourMMR: 0, theirMMR: 0 }), "CLOSE"); + assert.equal(calculateDifference({ ourMMR: 0, theirMMR: 1 }), "CLOSE"); + assert.equal(calculateDifference({ ourMMR: 1, theirMMR: 0 }), "CLOSE"); +}); + +CalculateDifference("Higher/lower", () => { + assert.equal(calculateDifference({ ourMMR: 0, theirMMR: 10_000 }), "HIGHER"); + assert.equal(calculateDifference({ ourMMR: 10_000, theirMMR: 0 }), "LOWER"); +}); + +CalculateDifference.only("A bit higher/lower", () => { + assert.equal( + calculateDifference({ ourMMR: 0, theirMMR: BIT_HIGHER_MMR_LIMIT }), + "BIT_HIGHER" + ); + assert.equal( + calculateDifference({ ourMMR: 0, theirMMR: -BIT_HIGHER_MMR_LIMIT }), + "BIT_LOWER" + ); +}); + UniteGroupInfo.run(); ScoresAreIdentical.run(); GroupsToWinningAndLosingPlayerIds.run(); +CalculateDifference.run(); diff --git a/app/core/play/utils.ts b/app/core/play/utils.ts index 40c142525..95104cb54 100644 --- a/app/core/play/utils.ts +++ b/app/core/play/utils.ts @@ -1,10 +1,22 @@ import invariant from "tiny-invariant"; -import { LFG_GROUP_FULL_SIZE, LFG_GROUP_INACTIVE_MINUTES } from "~/constants"; +import { + BIT_HIGHER_MMR_LIMIT, + CLOSE_MMR_LIMIT, + LFG_GROUP_FULL_SIZE, + LFG_GROUP_INACTIVE_MINUTES, +} from "~/constants"; import * as LFGGroup from "~/models/LFGGroup.server"; import { PlayFrontPageLoader } from "~/routes/play/index"; -import { LookingLoaderData } from "~/routes/play/looking"; +import { + LookingLoaderData, + LookingLoaderDataGroup, +} from "~/routes/play/looking"; import { Unpacked } from "~/utils"; -import { skillArrayToMMR, teamSkillToApproximateMMR } from "../mmr/utils"; +import { + skillArrayToMMR, + teamSkillToApproximateMMR, + teamSkillToExactMMR, +} from "../mmr/utils"; import { canUniteWithGroup } from "./validators"; export interface UniteGroupInfoArg { @@ -119,6 +131,7 @@ export function otherGroupsForResponse({ likes, lookingForMatch, ownGroup, + useRelativeSkillLevel, }: { groups: LFGGroup.FindLookingAndOwnActive; likes: { @@ -127,6 +140,7 @@ export function otherGroupsForResponse({ }; lookingForMatch: boolean; ownGroup: Unpacked; + useRelativeSkillLevel: boolean; }) { return groups .filter( @@ -140,7 +154,7 @@ export function otherGroupsForResponse({ ) .filter((group) => group.id !== ownGroup.id) .filter(filterExpiredGroups) - .map((group) => { + .map((group): LookingLoaderDataGroup => { const ranked = () => { if (lookingForMatch && !ownGroup.ranked) return false; @@ -162,8 +176,12 @@ export function otherGroupsForResponse({ }; }), ranked: ranked(), + MMRRelation: + ownGroup.ranked && group.ranked && useRelativeSkillLevel + ? resolveMMRRelation({ group, ownGroup }) + : undefined, teamMMR: - lookingForMatch && group.ranked + lookingForMatch && group.ranked && !useRelativeSkillLevel ? { exact: false, value: teamSkillToApproximateMMR(group.members), @@ -223,3 +241,35 @@ export function countGroups( { TWIN: 0, QUAD: 0, "VERSUS-RANKED": 0, "VERSUS-UNRANKED": 0 } ); } + +function resolveMMRRelation({ + group, + ownGroup, +}: { + group: Unpacked; + ownGroup: Unpacked; +}): NonNullable { + return calculateDifference({ + ourMMR: teamSkillToExactMMR(ownGroup.members), + theirMMR: teamSkillToExactMMR(group.members), + }); +} + +export function calculateDifference({ + ourMMR, + theirMMR, +}: { + ourMMR: number; + theirMMR: number; +}) { + const difference = Math.abs(ourMMR - theirMMR); + const ownIsBigger = ourMMR > theirMMR; + + if (difference <= CLOSE_MMR_LIMIT) return "CLOSE"; + if (difference > BIT_HIGHER_MMR_LIMIT && ownIsBigger) return "LOWER"; + if (difference > BIT_HIGHER_MMR_LIMIT && !ownIsBigger) return "HIGHER"; + if (ownIsBigger) return "BIT_LOWER"; + if (!ownIsBigger) return "BIT_HIGHER"; + + throw new Error("Unexpected calculateMMRRelation scenario"); +} diff --git a/app/routes/play/looking.tsx b/app/routes/play/looking.tsx index dd3e32cef..32667dac5 100644 --- a/app/routes/play/looking.tsx +++ b/app/routes/play/looking.tsx @@ -35,6 +35,8 @@ import { validate, } from "~/utils"; +const USE_RELATIVE_SKILL_LEVEL = true; + export const links: LinksFunction = () => { return [{ rel: "stylesheet", href: styles }]; }; @@ -215,6 +217,7 @@ export type LookingLoaderDataGroup = { exact: boolean; value: number; }; + MMRRelation?: "LOWER" | "BIT_LOWER" | "CLOSE" | "BIT_HIGHER" | "HIGHER"; ranked?: boolean; }; @@ -269,7 +272,7 @@ export const loader: LoaderFunction = async ({ context }) => { }), ranked: ownGroup.ranked ?? undefined, teamMMR: - lookingForMatch && isRanked + lookingForMatch && isRanked && !USE_RELATIVE_SKILL_LEVEL ? { exact: true, value: teamSkillToExactMMR(ownGroupWithMembers.members), @@ -280,6 +283,7 @@ export const loader: LoaderFunction = async ({ context }) => { isCaptain: isGroupAdmin({ group: ownGroup, user }), lastActionAtTimestamp: ownGroup.lastActionAt.getTime(), ...otherGroupsForResponse({ + useRelativeSkillLevel: USE_RELATIVE_SKILL_LEVEL, groups: groupsOfType, lookingForMatch, ownGroup, diff --git a/app/styles/play-layout.css b/app/styles/play-layout.css index c1457373d..6fddeed2d 100644 --- a/app/styles/play-layout.css +++ b/app/styles/play-layout.css @@ -99,6 +99,19 @@ font-size: var(--fonts-xxs); } +.play__card__mmr-relation { + display: flex; + width: 100%; + justify-content: center; + font-size: var(--fonts-sm); + font-weight: var(--semi-bold); + gap: var(--s-2); +} + +.play__card__mmr-relation > svg { + width: 1rem; +} + .play__card__member-power { display: flex; margin-top: -4px;