diff --git a/app/components/tournament/BracketActions.tsx b/app/components/tournament/BracketActions.tsx index 60fb40b7e..d9e1ac3fd 100644 --- a/app/components/tournament/BracketActions.tsx +++ b/app/components/tournament/BracketActions.tsx @@ -90,7 +90,7 @@ export function BracketActions() { ); invariant(matchWeAreWaitingFor, "matchWeAreWaitingFor is undefined"); - if (matchWeAreWaitingFor.participants?.length !== 2) { + if (matchWeAreWaitingFor.participants?.filter(Boolean).length !== 2) { return ( Waiting on match number {matchWeAreWaitingFor.number} (missing teams) @@ -104,7 +104,7 @@ export function BracketActions() { {matchWeAreWaitingFor.participants[1]} {(matchWeAreWaitingFor.score ?? [0, 0]).join("-")} - Best of{" "} - {matchWeAreWaitingFor.round.matches.length} + {matchWeAreWaitingFor.round.stages.length} ); diff --git a/app/models/TournamentBracket.ts b/app/models/TournamentBracket.ts index 9f0d949b0..9350fb523 100644 --- a/app/models/TournamentBracket.ts +++ b/app/models/TournamentBracket.ts @@ -26,6 +26,7 @@ export function findById(bracketId: string) { select: { team: { select: { + id: true, name: true, }, }, diff --git a/app/models/TournamentMatch.ts b/app/models/TournamentMatch.ts index 07cf5b60d..e59300ef1 100644 --- a/app/models/TournamentMatch.ts +++ b/app/models/TournamentMatch.ts @@ -58,3 +58,11 @@ export function createResult({ }, }); } + +export function createParticipants( + data: ({ matchId: string; order: TeamOrder; teamId: string } | undefined)[] +) { + return db.tournamentMatchParticipant.createMany({ + data: data.flatMap((result) => result ?? []), + }); +} diff --git a/app/routes/to/$organization.$tournament/bracket.$id.tsx b/app/routes/to/$organization.$tournament/bracket.$id.tsx index 3fb9ff7bc..82925f4f1 100644 --- a/app/routes/to/$organization.$tournament/bracket.$id.tsx +++ b/app/routes/to/$organization.$tournament/bracket.$id.tsx @@ -46,6 +46,7 @@ type ActionData = { }; export const action: ActionFunction = async ({ + params, request, context, }): Promise => { @@ -53,7 +54,7 @@ export const action: ActionFunction = async ({ request, schema: bracketActionSchema, }); - + invariant(typeof params.id === "string", "Expected params.id to be string"); const user = requireUser(context); switch (data._action) { @@ -64,6 +65,7 @@ export const action: ActionFunction = async ({ userId: user.id, winnerTeamId: data.winnerTeamId, position: data.position, + bracketId: params.id, }); return { ok: "REPORT_SCORE" }; } diff --git a/app/services/tournament.ts b/app/services/tournament.ts index 1ecd6879d..cedc9e938 100644 --- a/app/services/tournament.ts +++ b/app/services/tournament.ts @@ -1,4 +1,4 @@ -import type { Prisma, Stage, TournamentMatchGameResult } from ".prisma/client"; +import type { Prisma, Stage, TeamOrder } from ".prisma/client"; import { TOURNAMENT_CHECK_IN_CLOSING_MINUTES_FROM_START, TOURNAMENT_TEAM_ROSTER_MAX_SIZE, @@ -538,12 +538,14 @@ export async function reportScore({ matchId, playerIds, position, + bracketId, }: { userId: string; winnerTeamId: string; matchId: string; playerIds: string[]; position: number; + bracketId: string; }) { const match = await TournamentMatch.findById(matchId); if (!match) throw new Response("Invalid match id", { status: 400 }); @@ -574,17 +576,70 @@ export async function reportScore({ 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, - roundStageId: stage.id, - reporterId: userId, - winner: winnerTeam.order, - }); + // advance tournament after reporting score if match is over + if ( + matchIsOver( + match.round.stages.length, + matchResultsToTuple( + match.results + .map((r) => ({ winner: r.winner })) + .concat([{ winner: winnerTeam.order }]) + ) + ) + ) { + const loserTeam = match.participants.find((p) => p.teamId !== winnerTeamId); + invariant(loserTeam, "loserTeamId is undefined"); + + const bracket = await TournamentBracket.findById(bracketId); + if (!bracket) throw new Response("Invalid bracket id", { status: 400 }); + + return db.$transaction([ + TournamentMatch.createResult({ + matchId, + playerIds, + roundStageId: stage.id, + reporterId: userId, + winner: winnerTeam.order, + }), + // todo: bracket reset + TournamentMatch.createParticipants([ + match.winnerDestinationMatchId + ? { + matchId: match.winnerDestinationMatchId, + order: resolveNewOrder({ + bracket, + oldMatch: match, + newMatchId: match.winnerDestinationMatchId, + }), + teamId: winnerTeam.teamId, + } + : undefined, + match.loserDestinationMatchId + ? { + matchId: match.loserDestinationMatchId, + order: resolveNewOrder({ + bracket, + oldMatch: match, + newMatchId: match.loserDestinationMatchId, + }), + teamId: loserTeam?.teamId, + } + : undefined, + ]), + ]); + // otherwise if set is not over simply create result and return + } else { + return TournamentMatch.createResult({ + matchId, + playerIds, + roundStageId: stage.id, + reporterId: userId, + winner: winnerTeam.order, + }); + } } -function matchResultsToTuple(results: TournamentMatchGameResult[]) { +function matchResultsToTuple(results: { winner: TeamOrder }[]) { return results.reduce( (acc: [number, number], result) => { if (result.winner === "UPPER") acc[0]++; @@ -594,3 +649,44 @@ function matchResultsToTuple(results: TournamentMatchGameResult[]) { [0, 0] ); } + +function resolveNewOrder({ + bracket, + oldMatch, + newMatchId, +}: { + bracket: NonNullable; + oldMatch: NonNullable; + newMatchId: string; +}): TeamOrder { + const allMatches = bracket.rounds.flat().flatMap((round) => { + return round.matches; + }); + + const newMatch = allMatches.find((m) => m.id === newMatchId); + invariant(newMatch, "newMatch is undefined"); + + const matchesThatLeadToNewMatch = allMatches + .filter((m) => + [m.loserDestinationMatchId, m.winnerDestinationMatchId].includes( + newMatchId + ) + ) + .sort((a, b) => a.position - b.position); + invariant( + matchesThatLeadToNewMatch.length === 2, + `matchesThatLeadToNewMatch length was unexpected: ${matchesThatLeadToNewMatch.length}` + ); + console.log(JSON.stringify({ matchesThatLeadToNewMatch, oldMatch }, null, 2)); + invariant( + matchesThatLeadToNewMatch.find((m) => m.id === oldMatch.id), + "oldMatch not among matchesThatLeadToNewMatch" + ); + + // if match number is smaller it should mean the match is above + // the other match. thanks to sorting above 0 index should have + // the smaller match number. Winner's bracket match should + // always have the smaller number compared to loser's bracket match + if (matchesThatLeadToNewMatch[0].id === oldMatch.id) return "UPPER"; + return "LOWER"; +} diff --git a/app/styles/tournament-bracket.css b/app/styles/tournament-bracket.css index 9a483e328..9a35ee084 100644 --- a/app/styles/tournament-bracket.css +++ b/app/styles/tournament-bracket.css @@ -65,7 +65,7 @@ margin: 0 auto; column-gap: var(--s-6); grid-template-columns: 1fr 1fr; - row-gap: var(--s-2-5); + row-gap: var(--s-1-5); } .tournament-bracket__infos__label {