mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-22 11:05:09 -05:00
Show relative team MMR for ranked teams
This commit is contained in:
18
app/components/icons/ArrowDown.tsx
Normal file
18
app/components/icons/ArrowDown.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
export function ArrowDownIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
18
app/components/icons/ArrowUp.tsx
Normal file
18
app/components/icons/ArrowUp.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
export function ArrowUpIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 15l7-7 7 7"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
16
app/components/icons/DoubleArrowDown.tsx
Normal file
16
app/components/icons/DoubleArrowDown.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
export function DoubleArrowDownIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M15.707 4.293a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-5-5a1 1 0 011.414-1.414L10 8.586l4.293-4.293a1 1 0 011.414 0zm0 6a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-5-5a1 1 0 111.414-1.414L10 14.586l4.293-4.293a1 1 0 011.414 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
18
app/components/icons/DoubleArrowUp.tsx
Normal file
18
app/components/icons/DoubleArrowUp.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
export function DoubleArrowUpIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 11l7-7 7 7M5 19l7-7 7 7"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
18
app/components/icons/Minus.tsx
Normal file
18
app/components/icons/Minus.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
export function MinusIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M20 12H4"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
</div>
|
||||
)}
|
||||
{group.MMRRelation && <MMRRelation relation={group.MMRRelation} />}
|
||||
<input type="hidden" name="targetGroupId" value={group.id} />
|
||||
{action === "UNITE_GROUPS" && (
|
||||
<input
|
||||
@@ -96,3 +102,51 @@ export function GroupCard({
|
||||
</fetcher.Form>
|
||||
);
|
||||
}
|
||||
|
||||
function MMRRelation({
|
||||
relation,
|
||||
}: {
|
||||
relation: NonNullable<LookingLoaderDataGroup["MMRRelation"]>;
|
||||
}) {
|
||||
switch (relation) {
|
||||
case "CLOSE": {
|
||||
return (
|
||||
<div className="play__card__mmr-relation">
|
||||
<MinusIcon /> Close SP
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case "BIT_HIGHER": {
|
||||
return (
|
||||
<div className="play__card__mmr-relation">
|
||||
<ArrowUpIcon /> A bit higher SP
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case "BIT_LOWER": {
|
||||
return (
|
||||
<div className="play__card__mmr-relation">
|
||||
<ArrowDownIcon /> A bit lower SP
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case "HIGHER": {
|
||||
return (
|
||||
<div className="play__card__mmr-relation">
|
||||
<DoubleArrowUpIcon /> Higher SP
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case "LOWER": {
|
||||
return (
|
||||
<div className="play__card__mmr-relation">
|
||||
<DoubleArrowDownIcon /> Lower SP
|
||||
</div>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
const exhaustive: never = relation;
|
||||
throw new Error(`Unknown relation: ${JSON.stringify(exhaustive)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<LFGGroup.FindLookingAndOwnActive>;
|
||||
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<LFGGroup.FindLookingAndOwnActive>;
|
||||
ownGroup: Unpacked<LFGGroup.FindLookingAndOwnActive>;
|
||||
}): NonNullable<LookingLoaderDataGroup["MMRRelation"]> {
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user