From 10468ff8e0089b1d7b45518afeb730fcd9f6548d Mon Sep 17 00:00:00 2001
From: Kalle <38327916+Sendouc@users.noreply.github.com>
Date: Thu, 3 Mar 2022 17:39:01 +0200
Subject: [PATCH] Show "Replay" indicator in GroupCard
---
app/components/play/GroupCard.tsx | 5 ++-
app/core/play/utils.test.ts | 64 ++++++++++++++++++++++++++++++-
app/core/play/utils.ts | 34 ++++++++++++++++
app/models/LFGMatch.server.ts | 36 +++++++++++++++++
app/routes/play/looking.tsx | 9 ++++-
app/styles/play-layout.css | 6 +++
prisma/seed/script.ts | 64 +++++++++++++++++++++++++++----
7 files changed, 207 insertions(+), 11 deletions(-)
diff --git a/app/components/play/GroupCard.tsx b/app/components/play/GroupCard.tsx
index 417b533c5..27e522be2 100644
--- a/app/components/play/GroupCard.tsx
+++ b/app/components/play/GroupCard.tsx
@@ -74,7 +74,10 @@ export function GroupCard({
{group.teamMMR.value}
)}
- {group.MMRRelation && }
+ {group.MMRRelation && !group.replay && (
+
+ )}
+ {group.replay &&
Replay
}
{action === "UNITE_GROUPS" && (
{
assert.equal(calculateDifference({ ourMMR: 10_000, theirMMR: 0 }), "LOWER");
});
-CalculateDifference.only("A bit higher/lower", () => {
+CalculateDifference("A bit higher/lower", () => {
assert.equal(
calculateDifference({ ourMMR: 0, theirMMR: BIT_HIGHER_MMR_LIMIT }),
"BIT_HIGHER"
@@ -121,7 +123,67 @@ CalculateDifference.only("A bit higher/lower", () => {
);
});
+const user = { id: "a" };
+const createMembers = (input: string[]) => input.map((v) => ({ memberId: v }));
+
+IsMatchReplay("Detects replays", () => {
+ assert.ok(
+ isMatchReplay({
+ user,
+ recentMatch: {
+ groups: [
+ { members: createMembers(["a", "b", "c", "d"]) },
+ { members: createMembers(["e", "f", "g", "h"]) },
+ ],
+ },
+ group: { members: createMembers(["e", "f", "g", "h"]) },
+ })
+ );
+
+ assert.ok(
+ isMatchReplay({
+ user,
+ recentMatch: {
+ groups: [
+ { members: createMembers(["e", "f", "g", "h"]) },
+ { members: createMembers(["a", "b", "c", "d"]) },
+ ],
+ },
+ group: { members: createMembers(["e", "f", "g", "1"]) },
+ })
+ );
+});
+
+IsMatchReplay("Detects not replays", () => {
+ assert.not.ok(
+ isMatchReplay({
+ user,
+ recentMatch: {
+ groups: [
+ { members: createMembers(["a", "b", "c", "d"]) },
+ { members: createMembers(["e", "f", "g", "h"]) },
+ ],
+ },
+ group: { members: createMembers(["1", "2", "3", "4"]) },
+ })
+ );
+
+ assert.not.ok(
+ isMatchReplay({
+ user,
+ recentMatch: {
+ groups: [
+ { members: createMembers(["a", "b", "c", "d"]) },
+ { members: createMembers(["e", "f", "g", "h"]) },
+ ],
+ },
+ group: { members: createMembers(["e", "f", "3", "4"]) },
+ })
+ );
+});
+
UniteGroupInfo.run();
ScoresAreIdentical.run();
GroupsToWinningAndLosingPlayerIds.run();
CalculateDifference.run();
+IsMatchReplay.run();
diff --git a/app/core/play/utils.ts b/app/core/play/utils.ts
index fdf69e585..a718939cb 100644
--- a/app/core/play/utils.ts
+++ b/app/core/play/utils.ts
@@ -6,6 +6,7 @@ import {
LFG_GROUP_INACTIVE_MINUTES,
} from "~/constants";
import * as LFGGroup from "~/models/LFGGroup.server";
+import * as LFGMatch from "~/models/LFGMatch.server";
import { PlayFrontPageLoader } from "~/routes/play/index";
import {
LookingLoaderData,
@@ -132,6 +133,8 @@ export function otherGroupsForResponse({
lookingForMatch,
ownGroup,
useRelativeSkillLevel,
+ recentMatch,
+ user,
}: {
groups: LFGGroup.FindLookingAndOwnActive;
likes: {
@@ -141,6 +144,8 @@ export function otherGroupsForResponse({
lookingForMatch: boolean;
ownGroup: Unpacked;
useRelativeSkillLevel: boolean;
+ recentMatch: LFGMatch.RecentOfUser;
+ user: { id: string };
}) {
return (
groups
@@ -179,6 +184,7 @@ export function otherGroupsForResponse({
};
}),
ranked: ranked(),
+ replay: isMatchReplay({ user, group, recentMatch }),
MMRRelation:
ownGroup.ranked &&
group.ranked &&
@@ -220,6 +226,34 @@ export function otherGroupsForResponse({
);
}
+export function isMatchReplay({
+ recentMatch,
+ user,
+ group,
+}: {
+ recentMatch: { groups: { members: { memberId: string }[] }[] } | null;
+ user: { id: string };
+ group: { members: { memberId: string }[] };
+}): boolean {
+ if (!recentMatch) return false;
+
+ const opponentGroupOfRecent = recentMatch.groups.find((g) =>
+ g.members.every((m) => m.memberId !== user.id)
+ );
+ invariant(
+ opponentGroupOfRecent,
+ "Unexpected opponentGroupOfRecent undefined"
+ );
+
+ const memberIdsOfGroup = new Set(group.members.map((m) => m.memberId));
+ let sameCount = 0;
+ for (const { memberId } of opponentGroupOfRecent.members) {
+ if (memberIdsOfGroup.has(memberId)) sameCount++;
+ }
+
+ return sameCount > 2;
+}
+
export function filterExpiredGroups(group: { lastActionAt: Date }) {
const { EXPIRED: expiredDate } = groupExpiredDates();
diff --git a/app/models/LFGMatch.server.ts b/app/models/LFGMatch.server.ts
index cde0c56e1..16483e514 100644
--- a/app/models/LFGMatch.server.ts
+++ b/app/models/LFGMatch.server.ts
@@ -64,6 +64,42 @@ export function findByUserId({ userId }: { userId: string }) {
});
}
+export type RecentOfUser = Prisma.PromiseReturnType;
+export function recentOfUser(userId: string) {
+ const twoHoursAgo = () => {
+ const result = new Date();
+ result.setHours(result.getHours() - 2);
+
+ return result;
+ };
+ return db.lfgGroupMatch.findFirst({
+ where: {
+ groups: {
+ some: {
+ members: {
+ some: {
+ memberId: userId,
+ },
+ },
+ },
+ },
+ createdAt: {
+ gte: twoHoursAgo(),
+ },
+ },
+ orderBy: {
+ createdAt: "desc",
+ },
+ include: {
+ groups: {
+ include: {
+ members: true,
+ },
+ },
+ },
+ });
+}
+
export async function reportScore({
UNSAFE_matchId,
UNSAFE_winnerGroupIds,
diff --git a/app/routes/play/looking.tsx b/app/routes/play/looking.tsx
index db7b91de8..f7457632f 100644
--- a/app/routes/play/looking.tsx
+++ b/app/routes/play/looking.tsx
@@ -26,6 +26,7 @@ import {
import { canUniteWithGroup, isGroupAdmin } from "~/core/play/validators";
import { usePolling } from "~/hooks/common";
import * as LFGGroup from "~/models/LFGGroup.server";
+import * as LFGMatch from "~/models/LFGMatch.server";
import styles from "~/styles/play-looking.css";
import {
makeTitle,
@@ -219,6 +220,7 @@ export type LookingLoaderDataGroup = {
};
MMRRelation?: "LOWER" | "BIT_LOWER" | "CLOSE" | "BIT_HIGHER" | "HIGHER";
ranked?: boolean;
+ replay?: boolean;
};
export interface LookingLoaderData {
@@ -233,7 +235,10 @@ export interface LookingLoaderData {
export const loader: LoaderFunction = async ({ context }) => {
const user = requireUser(context);
- const { groups, ownGroup } = await LFGGroup.findLookingAndOwnActive(user.id);
+ const [{ groups, ownGroup }, recentMatch] = await Promise.all([
+ LFGGroup.findLookingAndOwnActive(user.id),
+ LFGMatch.recentOfUser(user.id),
+ ]);
if (!ownGroup) return redirect("/play");
if (ownGroup.status === "MATCH") {
@@ -283,6 +288,8 @@ export const loader: LoaderFunction = async ({ context }) => {
isCaptain: isGroupAdmin({ group: ownGroup, user }),
lastActionAtTimestamp: ownGroup.lastActionAt.getTime(),
...otherGroupsForResponse({
+ recentMatch,
+ user,
useRelativeSkillLevel: USE_RELATIVE_SKILL_LEVEL,
groups: groupsOfType,
lookingForMatch,
diff --git a/app/styles/play-layout.css b/app/styles/play-layout.css
index a2afb6aac..c1a38fcd5 100644
--- a/app/styles/play-layout.css
+++ b/app/styles/play-layout.css
@@ -116,6 +116,12 @@
width: 1rem;
}
+.play__card__replay {
+ color: var(--theme-info);
+ font-size: var(--fonts-sm);
+ font-weight: var(--semi-bold);
+}
+
.play__card__member-power {
display: flex;
margin-top: -4px;
diff --git a/prisma/seed/script.ts b/prisma/seed/script.ts
index 24d36a653..6d91fe848 100644
--- a/prisma/seed/script.ts
+++ b/prisma/seed/script.ts
@@ -80,6 +80,7 @@ export async function seed(variation?: SeedVariations) {
}
if (variation === "looking" || variation === "looking-match") {
await ownGroup(variation === "looking-match", remainingUserIdsForGroups);
+ await lfgMatches();
}
async function adminUser() {
@@ -405,19 +406,32 @@ export async function seed(variation?: SeedVariations) {
}
for (let i = 0; i < 24; i++) {
+ const members = {
+ createMany: {
+ data: new Array(4).fill(null).map((_, i) => ({
+ memberId: userIdsStack.shift()!,
+ captain: i === 0,
+ })),
+ },
+ };
+ if (i === 0) {
+ await prisma.lfgGroup.create({
+ data: {
+ id: `quad-${i + 1}-past`,
+ status: "INACTIVE",
+ type: "VERSUS",
+ ranked: true,
+ members,
+ },
+ });
+ }
await prisma.lfgGroup.create({
data: {
+ id: `quad-${i + 1}`,
status: "LOOKING",
type: "VERSUS",
ranked: i < 12,
- members: {
- createMany: {
- data: new Array(4).fill(null).map((_, i) => ({
- memberId: userIdsStack.shift()!,
- captain: i === 0,
- })),
- },
- },
+ members,
},
});
}
@@ -442,6 +456,20 @@ export async function seed(variation?: SeedVariations) {
}
}
+ await prisma.lfgGroup.create({
+ data: {
+ id: "own-group-past",
+ status: "INACTIVE",
+ type: "VERSUS",
+ ranked: true,
+ members: {
+ createMany: {
+ data: members,
+ },
+ },
+ },
+ });
+
return prisma.lfgGroup.create({
data: {
status: "LOOKING",
@@ -463,6 +491,26 @@ export async function seed(variation?: SeedVariations) {
});
}
+ async function lfgMatches() {
+ return prisma.lfgGroupMatch.create({
+ data: {
+ groups: {
+ connect: [{ id: "quad-1-past" }, { id: "own-group-past" }],
+ },
+ stages: {
+ createMany: {
+ data: [
+ { order: 1, stageId: 1, winnerGroupId: "own-group-past" },
+ { order: 2, stageId: 40, winnerGroupId: "own-group-past" },
+ { order: 3, stageId: 60, winnerGroupId: "own-group-past" },
+ { order: 4, stageId: 101, winnerGroupId: "own-group-past" },
+ ],
+ },
+ },
+ },
+ });
+ }
+
async function tournamentRoundsCreate() {
const stages = await prisma.stage.findMany({});
await createTournamentRounds({