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 (
+
+ );
+ }
+ case "BIT_LOWER": {
+ return (
+
+ );
+ }
+ 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;