Report score works without advance

This commit is contained in:
Kalle (Sendou)
2022-01-04 19:22:19 +02:00
parent 2c886a3739
commit 460fa344b9
11 changed files with 83 additions and 32 deletions

View File

@@ -43,7 +43,11 @@ export function DuringMatchActions({
});
if (joinedRoom) {
const currentStage = currentRound.stages.find((m) => m.position === 1);
const currentPosition =
currentMatch.score?.reduce((acc, cur) => acc + cur, 1) ?? 1;
const currentStage = currentRound.stages.find(
(m) => m.position === currentPosition
);
invariant(currentStage, "currentStage is undefined");
const { stage } = currentStage;
@@ -66,9 +70,7 @@ export function DuringMatchActions({
/>
{modesShortToLong[stage.mode]} on {stage.name}
</h4>
<h4>
Stage {currentMatch.score?.reduce((acc, cur) => acc + cur, 1)}
</h4>
<h4>Stage {currentPosition}</h4>
</div>
</div>
<ActionSectionWrapper justify-center>
@@ -90,6 +92,7 @@ export function DuringMatchActions({
ownTeam={ownTeam}
opponentTeam={opponentTeam}
matchId={currentMatch.id}
position={currentPosition}
/>
</ActionSectionWrapper>
</div>

View File

@@ -11,10 +11,12 @@ export function DuringMatchActionsRosters({
ownTeam,
opponentTeam,
matchId,
position,
}: {
ownTeam: Unpacked<FindTournamentByNameForUrlI["teams"]>;
opponentTeam: Unpacked<FindTournamentByNameForUrlI["teams"]>;
matchId: string;
position: number;
}) {
const [checkedPlayers, setCheckedPlayers] = React.useState<
[string[], string[]]
@@ -89,6 +91,7 @@ export function DuringMatchActionsRosters({
name="playerIds"
value={JSON.stringify(checkedPlayers.flat())}
/>
<input type="hidden" name="position" value={position} />
<ReportScoreButtons
checkedPlayers={checkedPlayers}
winnerName={winningTeam()}

View File

@@ -74,6 +74,8 @@ export const TOURNAMENT_TEAM_ROSTER_MIN_SIZE = 4;
export const TOURNAMENT_TEAM_ROSTER_MAX_SIZE = 6;
/** How many minutes before the start of the tournament check-in closes */
export const TOURNAMENT_CHECK_IN_CLOSING_MINUTES_FROM_START = 10;
export const BEST_OF_OPTIONS = [3, 5, 7, 9] as const;
export const checkInClosesDate = (startTime: string): Date => {
return new Date(new Date(startTime).getTime() - 1000 * 10);
};

View File

@@ -26,13 +26,13 @@ export function findById(id: string) {
}
export function createResult({
position,
roundStageId,
reporterId,
winner,
matchId,
playerIds,
}: {
position: number;
roundStageId: string;
reporterId: string;
winner: TeamOrder;
matchId: string;
@@ -40,7 +40,11 @@ export function createResult({
}) {
return db.tournamentMatchGameResult.create({
data: {
position,
roundStage: {
connect: {
id: roundStageId,
},
},
reporterId,
winner,
players: {

View File

@@ -12,7 +12,8 @@ import type { BracketModified } from "~/services/tournament";
import { useUser } from "~/utils/hooks";
import { BracketActions } from "~/components/tournament/BracketActions";
import { z } from "zod";
import { parseRequestFormData, requireUser } from "~/utils";
import { parseRequestFormData, requireUser, safeJSONParse } from "~/utils";
import { BEST_OF_OPTIONS, TOURNAMENT_TEAM_ROSTER_MIN_SIZE } from "~/constants";
export const links: LinksFunction = () => {
return [{ rel: "stylesheet", href: styles }];
@@ -23,9 +24,16 @@ const bracketActionSchema = z.union([
_action: z.literal("REPORT_SCORE"),
matchId: z.string().uuid(),
winnerTeamId: z.string().uuid(),
position: z.preprocess(
Number,
z
.number()
.min(1)
.max(Math.max(...BEST_OF_OPTIONS))
),
playerIds: z.preprocess(
(val) => (val ? JSON.parse(val as any) : undefined),
z.array(z.string().uuid()).length(8)
safeJSONParse,
z.array(z.string().uuid()).length(TOURNAMENT_TEAM_ROSTER_MIN_SIZE * 2)
),
}),
z.object({
@@ -55,6 +63,7 @@ export const action: ActionFunction = async ({
playerIds: data.playerIds,
userId: user.id,
winnerTeamId: data.winnerTeamId,
position: data.position,
});
return { ok: "REPORT_SCORE" };
}

View File

@@ -16,7 +16,7 @@ import { Alert } from "~/components/Alert";
import { Button } from "~/components/Button";
import { Catcher } from "~/components/Catcher";
import { RefreshIcon } from "~/components/icons/Refresh";
import { modesShort, modesShortToLong } from "~/constants";
import { BEST_OF_OPTIONS, modesShort, modesShortToLong } from "~/constants";
import { eliminationBracket } from "~/core/tournament/algorithms";
import {
EliminationBracketSide,
@@ -229,7 +229,7 @@ function RoundsCollection({
<section key={round.name} className="tournament__start__round">
<h4>{round.name}</h4>
<div className="tournament__start__best-of-buttons-container">
{([3, 5, 7, 9] as const).map((bestOf) => (
{BEST_OF_OPTIONS.map((bestOf) => (
<button
key={bestOf}
type="button"

View File

@@ -24,6 +24,7 @@ import * as TrustRelationship from "~/models/TrustRelationship";
import * as TournamentMatch from "~/models/TournamentMatch";
import { Serialized, Unpacked } from "~/utils";
import { db } from "~/utils/db.server";
import invariant from "tiny-invariant";
export type FindTournamentByNameForUrlI = Serialized<
Prisma.PromiseReturnType<typeof findTournamentByNameForUrl>
@@ -536,11 +537,13 @@ export async function reportScore({
winnerTeamId,
matchId,
playerIds,
position,
}: {
userId: string;
winnerTeamId: string;
matchId: string;
playerIds: string[];
position: number;
}) {
const match = await TournamentMatch.findById(matchId);
if (!match) throw new Response("Invalid match id", { status: 400 });
@@ -552,7 +555,8 @@ export async function reportScore({
) {
throw new Response("No permissions to report score", { status: 401 });
}
if (match.results.some((result) => result.matchId === matchId)) {
if (position <= match.results.length) {
// no throw so it's handled gracefully if both teams report the score at the same time
return;
}
@@ -567,11 +571,14 @@ export async function reportScore({
throw new Response("Invalid winner team id", { status: 400 });
}
const stage = match.round.stages.find((stage) => stage.position === position);
invariant(stage, "stage is undefined");
// TODO transaction advance bracket conditionally
return TournamentMatch.createResult({
matchId,
playerIds,
position: match.position,
roundStageId: stage.id,
reporterId: userId,
winner: winnerTeam.order,
});

View File

@@ -64,6 +64,14 @@ export async function parseRequestFormData<T extends z.ZodTypeAny>({
}
}
export function safeJSONParse(value: any) {
try {
return JSON.parse(value);
} catch (e) {
return undefined;
}
}
/** @link https://stackoverflow.com/a/69413184 */
// @ts-expect-error
export const assertType = <A, B extends A>() => {};

View File

@@ -112,9 +112,12 @@ CREATE TABLE "TournamentRound" (
-- CreateTable
CREATE TABLE "TournamentRoundStage" (
"id" TEXT NOT NULL,
"position" INTEGER NOT NULL,
"roundId" TEXT NOT NULL,
"stageId" INTEGER NOT NULL
"stageId" INTEGER NOT NULL,
CONSTRAINT "TournamentRoundStage_pkey" PRIMARY KEY ("id")
);
-- CreateTable
@@ -139,7 +142,7 @@ CREATE TABLE "TournamentMatchParticipant" (
CREATE TABLE "TournamentMatchGameResult" (
"id" TEXT NOT NULL,
"matchId" TEXT NOT NULL,
"position" INTEGER NOT NULL,
"roundStageId" TEXT NOT NULL,
"winner" "TeamOrder" NOT NULL,
"reporterId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -187,7 +190,7 @@ CREATE UNIQUE INDEX "TournamentRoundStage_position_roundId_key" ON "TournamentRo
CREATE UNIQUE INDEX "TournamentMatchParticipant_teamId_matchId_key" ON "TournamentMatchParticipant"("teamId", "matchId");
-- CreateIndex
CREATE UNIQUE INDEX "TournamentMatchGameResult_matchId_position_key" ON "TournamentMatchGameResult"("matchId", "position");
CREATE UNIQUE INDEX "TournamentMatchGameResult_matchId_roundStageId_key" ON "TournamentMatchGameResult"("matchId", "roundStageId");
-- CreateIndex
CREATE UNIQUE INDEX "_TournamentMatchGameResultToUser_AB_unique" ON "_TournamentMatchGameResultToUser"("A", "B");
@@ -252,6 +255,9 @@ ALTER TABLE "TournamentMatchParticipant" ADD CONSTRAINT "TournamentMatchParticip
-- AddForeignKey
ALTER TABLE "TournamentMatchGameResult" ADD CONSTRAINT "TournamentMatchGameResult_matchId_fkey" FOREIGN KEY ("matchId") REFERENCES "TournamentMatch"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TournamentMatchGameResult" ADD CONSTRAINT "TournamentMatchGameResult_roundStageId_fkey" FOREIGN KEY ("roundStageId") REFERENCES "TournamentRoundStage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_TournamentMatchGameResultToUser" ADD FOREIGN KEY ("A") REFERENCES "TournamentMatchGameResult"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -146,12 +146,14 @@ model TournamentRound {
}
model TournamentRoundStage {
id String @id @default(uuid())
// position of the match 1 for Game 1, 2 for Game 2 etc.
position Int
roundId String
stageId Int
round TournamentRound @relation(fields: [roundId], references: [id])
stage Stage @relation(fields: [stageId], references: [id])
round TournamentRound @relation(fields: [roundId], references: [id])
stage Stage @relation(fields: [stageId], references: [id])
results TournamentMatchGameResult[]
@@unique([position, roundId])
}
@@ -188,14 +190,15 @@ model TournamentMatchParticipant {
}
model TournamentMatchGameResult {
id String @id @default(uuid())
matchId String
position Int
winner TeamOrder
reporterId String
createdAt DateTime @default(now())
match TournamentMatch @relation(fields: [matchId], references: [id])
players User[]
id String @id @default(uuid())
matchId String
roundStageId String
winner TeamOrder
reporterId String
createdAt DateTime @default(now())
match TournamentMatch @relation(fields: [matchId], references: [id])
players User[]
roundStage TournamentRoundStage @relation(fields: [roundStageId], references: [id])
@@unique([matchId, position])
@@unique([matchId, roundStageId])
}

View File

@@ -375,7 +375,7 @@ export async function seed(variation?: "check-in" | "match") {
async function advanceRound() {
const matches = await prisma.tournamentMatch.findMany({
include: { participants: true },
include: { participants: true, round: { include: { stages: true } } },
});
const matchToAdvance = matches.find((match) => match.position === 1);
invariant(matchToAdvance);
@@ -385,19 +385,25 @@ export async function seed(variation?: "check-in" | "match") {
{
matchId: matchToAdvance.id,
winner: "LOWER",
position: 1,
roundStageId: matchToAdvance.round.stages.find(
(stage) => stage.position === 1
)!.id,
reporterId: "",
},
{
matchId: matchToAdvance.id,
winner: "UPPER",
position: 2,
roundStageId: matchToAdvance.round.stages.find(
(stage) => stage.position === 2
)!.id,
reporterId: "",
},
{
matchId: matchToAdvance.id,
winner: "LOWER",
position: 3,
roundStageId: matchToAdvance.round.stages.find(
(stage) => stage.position === 3
)!.id,
reporterId: "",
},
],