mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-04-24 23:19:39 -05:00
Show warning if reports different score
This commit is contained in:
parent
4f4473b355
commit
fb1b3deba1
|
|
@ -1,8 +1,9 @@
|
|||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import { uniteGroupInfo, UniteGroupInfoArg } from "./utils";
|
||||
import { scoresAreIdentical, uniteGroupInfo, UniteGroupInfoArg } from "./utils";
|
||||
|
||||
const UniteGroupInfo = suite("uniteGroupInfo()");
|
||||
const ScoresAreIdentical = suite("scoresAreIdentical()");
|
||||
|
||||
const SMALL_GROUP: UniteGroupInfoArg = { id: "small", memberCount: 1 };
|
||||
const BIG_GROUP: UniteGroupInfoArg = { id: "big", memberCount: 3 };
|
||||
|
|
@ -29,4 +30,45 @@ UniteGroupInfo("Bigger group survives", () => {
|
|||
assert.equal(otherGroupId, "small");
|
||||
});
|
||||
|
||||
ScoresAreIdentical("Detects identical score", () => {
|
||||
const result = scoresAreIdentical({
|
||||
stages: [
|
||||
{ winnerGroupId: "a" },
|
||||
{ winnerGroupId: "a" },
|
||||
{ winnerGroupId: "a" },
|
||||
],
|
||||
winnerIds: ["a", "a", "a"],
|
||||
});
|
||||
|
||||
assert.ok(result);
|
||||
});
|
||||
|
||||
ScoresAreIdentical("Detects not identical score", () => {
|
||||
const result = scoresAreIdentical({
|
||||
stages: [
|
||||
{ winnerGroupId: "a" },
|
||||
{ winnerGroupId: "a" },
|
||||
{ winnerGroupId: "b" },
|
||||
],
|
||||
winnerIds: ["a", "b", "a"],
|
||||
});
|
||||
const result2 = scoresAreIdentical({
|
||||
stages: [
|
||||
{ winnerGroupId: "a" },
|
||||
{ winnerGroupId: "a" },
|
||||
{ winnerGroupId: "b" },
|
||||
],
|
||||
winnerIds: ["b", "b", "a"],
|
||||
});
|
||||
const result3 = scoresAreIdentical({
|
||||
stages: [{ winnerGroupId: "a" }, { winnerGroupId: "a" }],
|
||||
winnerIds: ["a", "a", "a"],
|
||||
});
|
||||
|
||||
assert.not.ok(result);
|
||||
assert.not.ok(result2);
|
||||
assert.not.ok(result3);
|
||||
});
|
||||
|
||||
UniteGroupInfo.run();
|
||||
ScoresAreIdentical.run();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { UniteGroupsArgs } from "~/models/LFGGroup.server";
|
||||
import * as LFGMatch from "~/models/LFGMatch.server";
|
||||
|
||||
export interface UniteGroupInfoArg {
|
||||
id: string;
|
||||
|
|
@ -18,3 +19,23 @@ export function uniteGroupInfo(
|
|||
removeCaptainsFromOther: groupA.memberCount !== groupB.memberCount,
|
||||
};
|
||||
}
|
||||
|
||||
/** Checks if the reported score is the same as score from the database */
|
||||
export function scoresAreIdentical({
|
||||
stages,
|
||||
winnerIds,
|
||||
}: {
|
||||
stages: { winnerGroupId: string | null }[];
|
||||
winnerIds: string[];
|
||||
}): boolean {
|
||||
const stagesWithWinner = stages.filter((stage) => stage.winnerGroupId);
|
||||
if (stagesWithWinner.length !== winnerIds.length) return false;
|
||||
|
||||
for (const [i, stage] of stagesWithWinner.entries()) {
|
||||
if (!stage.winnerGroupId) break;
|
||||
|
||||
if (stage.winnerGroupId !== winnerIds[i]) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import type { Prisma } from "@prisma/client";
|
||||
import { db } from "~/utils/db.server";
|
||||
|
||||
export type FindById = Prisma.PromiseReturnType<typeof findById>;
|
||||
export function findById(id: string) {
|
||||
return db.lfgGroupMatch.findUnique({
|
||||
where: { id },
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
json,
|
||||
LinksFunction,
|
||||
LoaderFunction,
|
||||
MetaFunction,
|
||||
redirect,
|
||||
useLoaderData,
|
||||
} from "remix";
|
||||
|
|
@ -17,7 +18,13 @@ import { uniteGroupInfo } from "~/core/play/utils";
|
|||
import { canUniteWithGroup, isGroupAdmin } from "~/core/play/validators";
|
||||
import * as LFGGroup from "~/models/LFGGroup.server";
|
||||
import styles from "~/styles/play-looking.css";
|
||||
import { parseRequestFormData, requireUser, UserLean, validate } from "~/utils";
|
||||
import {
|
||||
makeTitle,
|
||||
parseRequestFormData,
|
||||
requireUser,
|
||||
UserLean,
|
||||
validate,
|
||||
} from "~/utils";
|
||||
import {
|
||||
skillToMMR,
|
||||
teamSkillToApproximateMMR,
|
||||
|
|
@ -28,6 +35,15 @@ export const links: LinksFunction = () => {
|
|||
return [{ rel: "stylesheet", href: styles }];
|
||||
};
|
||||
|
||||
export const meta: MetaFunction = ({ data }: { data: LookingLoaderData }) => {
|
||||
return {
|
||||
title: makeTitle([
|
||||
`(${data.likedGroups.length}/${data.neutralGroups.length}/${data.likerGroups.length})`,
|
||||
"Looking",
|
||||
]),
|
||||
};
|
||||
};
|
||||
|
||||
export type LookingActionSchema = z.infer<typeof lookingActionSchema>;
|
||||
const lookingActionSchema = z.union([
|
||||
z.object({
|
||||
|
|
@ -160,6 +176,7 @@ interface LookingLoaderData {
|
|||
isCaptain: boolean;
|
||||
}
|
||||
|
||||
// TODO: show unranked when looking for match as ranked
|
||||
export const loader: LoaderFunction = async ({ context }) => {
|
||||
const user = requireUser(context);
|
||||
const ownGroup = await LFGGroup.findActiveByMember(user);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
LoaderFunction,
|
||||
MetaFunction,
|
||||
redirect,
|
||||
useActionData,
|
||||
useLoaderData,
|
||||
useTransition,
|
||||
} from "remix";
|
||||
|
|
@ -19,6 +20,8 @@ import { CheckmarkIcon } from "~/components/icons/Checkmark";
|
|||
import { ModeImage } from "~/components/ModeImage";
|
||||
import { MapList } from "~/components/play/MapList";
|
||||
import { DISCORD_URL } from "~/constants";
|
||||
import { scoresAreIdentical } from "~/core/play/utils";
|
||||
import { isGroupAdmin } from "~/core/play/validators";
|
||||
import * as LFGGroup from "~/models/LFGGroup.server";
|
||||
import * as LFGMatch from "~/models/LFGMatch.server";
|
||||
import styles from "~/styles/play-match.css";
|
||||
|
|
@ -64,7 +67,15 @@ const matchActionSchema = z.union([
|
|||
}),
|
||||
]);
|
||||
|
||||
export const action: ActionFunction = async ({ request, context }) => {
|
||||
type ActionData = {
|
||||
error?: "DIFFERENT_SCORE";
|
||||
ok?: z.infer<typeof matchActionSchema>["_action"];
|
||||
};
|
||||
|
||||
export const action: ActionFunction = async ({
|
||||
request,
|
||||
context,
|
||||
}): Promise<ActionData | Response> => {
|
||||
const data = await parseRequestFormData({
|
||||
request,
|
||||
schema: matchActionSchema,
|
||||
|
|
@ -76,7 +87,24 @@ export const action: ActionFunction = async ({ request, context }) => {
|
|||
|
||||
switch (data._action) {
|
||||
case "REPORT_SCORE": {
|
||||
validate(ownGroup.matchId, "No match for the group");
|
||||
validate(ownGroup.matchId, "Group doesn't have a match");
|
||||
validate(isGroupAdmin({ group: ownGroup, user }), "Not group admin");
|
||||
const match = await LFGMatch.findById(ownGroup.matchId);
|
||||
if (match?.stages.some((stage) => stage.winnerGroupId)) {
|
||||
// just don't do anything if they report same as someone else before them
|
||||
// to user it looks identical to if they were the first to submit
|
||||
if (
|
||||
scoresAreIdentical({
|
||||
stages: match.stages,
|
||||
winnerIds: data.winnerIds,
|
||||
})
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
return { error: "DIFFERENT_SCORE" };
|
||||
}
|
||||
|
||||
await LFGMatch.reportScore({
|
||||
UNSAFE_matchId: ownGroup.matchId,
|
||||
UNSAFE_winnerIds: data.winnerIds,
|
||||
|
|
@ -190,12 +218,21 @@ export const loader: LoaderFunction = async ({ params, context }) => {
|
|||
});
|
||||
};
|
||||
|
||||
// TODO: match time
|
||||
export default function LFGMatchPage() {
|
||||
const data = useLoaderData<LFGMatchLoaderData>();
|
||||
const transition = useTransition();
|
||||
const actionData = useActionData<ActionData>();
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
{actionData?.error === "DIFFERENT_SCORE" && (
|
||||
<div className="play-match__error">
|
||||
The score you reported is different from what your opponent reported.
|
||||
If you think the information below is wrong notify us on the #helpdesk
|
||||
channel of our <a href={DISCORD_URL}>Discord</a> channel
|
||||
</div>
|
||||
)}
|
||||
<div className="play-match__waves">
|
||||
<div className="play-match__teams">
|
||||
{data.groups.map((g, i) => {
|
||||
|
|
@ -239,7 +276,7 @@ export default function LFGMatchPage() {
|
|||
<>
|
||||
<div
|
||||
className={clsx("play-match__checkmark", "left", {
|
||||
invisible: stage.winner === 0,
|
||||
invisible: stage.winner !== 0,
|
||||
})}
|
||||
>
|
||||
<CheckmarkIcon />
|
||||
|
|
@ -253,7 +290,7 @@ export default function LFGMatchPage() {
|
|||
</div>
|
||||
<div
|
||||
className={clsx("play-match__checkmark", {
|
||||
invisible: stage.winner === 1,
|
||||
invisible: stage.winner !== 1,
|
||||
})}
|
||||
>
|
||||
<CheckmarkIcon />
|
||||
|
|
|
|||
|
|
@ -1,8 +1,16 @@
|
|||
.play-match__error {
|
||||
padding: var(--s-4);
|
||||
background-color: var(--bg-lighter);
|
||||
border-radius: var(--rounded);
|
||||
color: var(--theme-warning);
|
||||
}
|
||||
|
||||
.play-match__waves {
|
||||
padding: var(--s-8);
|
||||
background: url("/svg/background-pattern.svg");
|
||||
background-color: var(--bg-lighter);
|
||||
border-radius: var(--rounded);
|
||||
margin-block-start: var(--s-4);
|
||||
}
|
||||
|
||||
.play-match__teams {
|
||||
|
|
@ -53,6 +61,7 @@
|
|||
padding: var(--s-1);
|
||||
background-color: var(--bg-darker);
|
||||
border-radius: var(--rounded);
|
||||
padding-inline: var(--s-3);
|
||||
}
|
||||
|
||||
.play-match__played-mode {
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@ import type { EventTargetRecorder } from "server/events";
|
|||
import { z } from "zod";
|
||||
import { LoggedInUserSchema } from "~/utils/schemas";
|
||||
|
||||
export function makeTitle(endOfTitle?: string) {
|
||||
return endOfTitle ? `sendou.ink | ${endOfTitle}` : "sendou.ink";
|
||||
export function makeTitle(title?: string | string[]) {
|
||||
if (!title) return "sendou.ink";
|
||||
if (typeof title === "string") return `${title} | sendou.ink`;
|
||||
return `${title.join(" | ")} | sendou.ink`;
|
||||
}
|
||||
|
||||
/** Get logged in user from context. Throws with 401 error if no user found. */
|
||||
|
|
|
|||
|
|
@ -184,6 +184,7 @@ CREATE TABLE "LfgGroupMember" (
|
|||
-- CreateTable
|
||||
CREATE TABLE "LfgGroupMatch" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "LfgGroupMatch_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
|
@ -209,6 +210,18 @@ CREATE TABLE "Skill" (
|
|||
CONSTRAINT "Skill_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "GroupSkill" (
|
||||
"id" TEXT NOT NULL,
|
||||
"mu" DOUBLE PRECISION NOT NULL,
|
||||
"sigma" DOUBLE PRECISION NOT NULL,
|
||||
"matchId" TEXT,
|
||||
"tournamentId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "GroupSkill_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_StageToTournament" (
|
||||
"A" INTEGER NOT NULL,
|
||||
|
|
@ -221,6 +234,12 @@ CREATE TABLE "_TournamentMatchGameResultToUser" (
|
|||
"B" TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "_GroupSkillToUser" (
|
||||
"A" TEXT NOT NULL,
|
||||
"B" TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_discordId_key" ON "User"("discordId");
|
||||
|
||||
|
|
@ -281,6 +300,12 @@ CREATE UNIQUE INDEX "_TournamentMatchGameResultToUser_AB_unique" ON "_Tournament
|
|||
-- CreateIndex
|
||||
CREATE INDEX "_TournamentMatchGameResultToUser_B_index" ON "_TournamentMatchGameResultToUser"("B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "_GroupSkillToUser_AB_unique" ON "_GroupSkillToUser"("A", "B");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "_GroupSkillToUser_B_index" ON "_GroupSkillToUser"("B");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Organization" ADD CONSTRAINT "Organization_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
|
|
@ -368,6 +393,12 @@ ALTER TABLE "Skill" ADD CONSTRAINT "Skill_tournamentId_fkey" FOREIGN KEY ("tourn
|
|||
-- AddForeignKey
|
||||
ALTER TABLE "Skill" ADD CONSTRAINT "Skill_matchId_fkey" FOREIGN KEY ("matchId") REFERENCES "LfgGroupMatch"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "GroupSkill" ADD CONSTRAINT "GroupSkill_tournamentId_fkey" FOREIGN KEY ("tournamentId") REFERENCES "Tournament"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "GroupSkill" ADD CONSTRAINT "GroupSkill_matchId_fkey" FOREIGN KEY ("matchId") REFERENCES "LfgGroupMatch"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_StageToTournament" ADD FOREIGN KEY ("A") REFERENCES "Stage"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
|
|
@ -379,3 +410,9 @@ ALTER TABLE "_TournamentMatchGameResultToUser" ADD FOREIGN KEY ("A") REFERENCES
|
|||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_TournamentMatchGameResultToUser" ADD FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_GroupSkillToUser" ADD FOREIGN KEY ("A") REFERENCES "GroupSkill"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "_GroupSkillToUser" ADD FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
|
@ -27,6 +27,7 @@ model User {
|
|||
tournamentMatches TournamentMatchGameResult[]
|
||||
lfgGroups LfgGroupMember[]
|
||||
skill Skill[]
|
||||
groupSkills GroupSkill[]
|
||||
}
|
||||
|
||||
model Organization {
|
||||
|
|
@ -42,27 +43,28 @@ model Organization {
|
|||
}
|
||||
|
||||
model Tournament {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
// Name in lower case to show in URL
|
||||
nameForUrl String
|
||||
description String
|
||||
startTime DateTime
|
||||
checkInStartTime DateTime
|
||||
nameForUrl String
|
||||
description String
|
||||
startTime DateTime
|
||||
checkInStartTime DateTime
|
||||
// CSS for tournament banner's background value
|
||||
bannerBackground String
|
||||
bannerBackground String
|
||||
// CSS for tournament banner's color value
|
||||
bannerTextHSLArgs String
|
||||
bannerTextHSLArgs String
|
||||
// Team ID's in an array in the order of the seed. 0 index is 1st seed, 1 index is 2nd seed etc.
|
||||
// Any team not in the list will be at the bottom in the order of their createdAt time stamp
|
||||
// (older teams first)
|
||||
seeds String[]
|
||||
mapPool Stage[]
|
||||
organizerId String
|
||||
organizer Organization @relation(fields: [organizerId], references: [id])
|
||||
teams TournamentTeam[]
|
||||
brackets TournamentBracket[]
|
||||
changedSkills Skill[]
|
||||
seeds String[]
|
||||
mapPool Stage[]
|
||||
organizerId String
|
||||
organizer Organization @relation(fields: [organizerId], references: [id])
|
||||
teams TournamentTeam[]
|
||||
brackets TournamentBracket[]
|
||||
changedSkills Skill[]
|
||||
changedGroupSkills GroupSkill[]
|
||||
|
||||
// There might be duplicate nameForUrl's but inside an organization they're unique
|
||||
@@unique([nameForUrl, organizerId])
|
||||
|
|
@ -255,10 +257,12 @@ model LfgGroupMember {
|
|||
}
|
||||
|
||||
model LfgGroupMatch {
|
||||
id String @id @default(uuid())
|
||||
groups LfgGroup[]
|
||||
changedSkills Skill[]
|
||||
stages LfgGroupMatchStage[]
|
||||
id String @id @default(uuid())
|
||||
createdAt DateTime @default(now())
|
||||
groups LfgGroup[]
|
||||
changedSkills Skill[]
|
||||
stages LfgGroupMatchStage[]
|
||||
changedGroupSkills GroupSkill[]
|
||||
}
|
||||
|
||||
model LfgGroupMatchStage {
|
||||
|
|
@ -285,3 +289,15 @@ model Skill {
|
|||
match LfgGroupMatch? @relation(fields: [matchId], references: [id])
|
||||
tournament Tournament? @relation(fields: [tournamentId], references: [id])
|
||||
}
|
||||
|
||||
model GroupSkill {
|
||||
id String @id @default(uuid())
|
||||
mu Float
|
||||
sigma Float
|
||||
matchId String?
|
||||
tournamentId String?
|
||||
createdAt DateTime @default(now())
|
||||
users User[]
|
||||
match LfgGroupMatch? @relation(fields: [matchId], references: [id])
|
||||
tournament Tournament? @relation(fields: [tournamentId], references: [id])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ app.all(
|
|||
}
|
||||
);
|
||||
|
||||
const port = process.env.PORT || 3000;
|
||||
const port = process.env.PORT ?? 5800;
|
||||
app.listen(port, () => {
|
||||
console.log(`Express server listening on port ${port}`);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user