Tournament bracket working with data

This commit is contained in:
Kalle (Sendou)
2021-12-23 13:58:18 +02:00
parent aed3023832
commit 7914377309
7 changed files with 145 additions and 128 deletions

View File

@@ -1,126 +1,96 @@
import classNames from "classnames";
import type { BracketModified } from "~/services/tournament";
import { Unpacked } from "~/utils";
const rounds = [
{
title: "Round 1",
bestOf: 3,
status: "DONE",
},
{
title: "Round 2",
bestOf: 5,
status: "INPROGRESS",
},
{
title: "Semifinals",
bestOf: 5,
status: "UPCOMING",
},
{
title: "Finals",
bestOf: 7,
status: "UPCOMING",
},
] as const;
export function EliminationBracket() {
export function EliminationBracket({
bracketSide,
}: {
bracketSide: BracketModified["winners"];
}) {
return (
<div
className="tournament-bracket__elim__container"
style={
{
"--brackets-columns": rounds.length,
"--brackets-matches": 4,
"--brackets-columns": bracketSide.length,
"--brackets-max-matches": bracketSide[0].matches.length,
} as any
}
>
<div className="tournament-bracket__elim__bracket">
{rounds.map((round, i) => (
{bracketSide.map((round, i) => (
<RoundInfo
key={round.title}
round={round}
isLast={i === rounds.length - 1}
key={round.id}
title={round.name}
isLast={i === bracketSide.length - 1}
bestOf={round.bestOf}
status="UPCOMING"
/>
))}
<div
className="tournament-bracket__elim__column"
style={{ "--brackets-column-matches": 4 } as any}
>
<div className="tournament-bracket__elim__matches">
<Match />
<Match />
<Match />
<Match />
</div>
<div className="tournament-bracket__elim__lines">
<div />
<div />
</div>
</div>
<div
className="tournament-bracket__elim__column"
style={{ "--brackets-column-matches": 2 } as any}
>
<div className="tournament-bracket__elim__matches">
<Match />
<Match />
</div>
<div className="tournament-bracket__elim__lines">
<div />
</div>
</div>
<div
className="tournament-bracket__elim__column"
style={
{
"--brackets-column-matches": 0,
"--brackets-bottom-border-length": 0,
} as any
}
>
<div className="tournament-bracket__elim__matches">
<Match />
</div>
<div className="tournament-bracket__elim__lines">
<div />
</div>
</div>
<div className="tournament-bracket__elim__matches">
<Match />
</div>
{bracketSide.map((round, i) => {
return (
<div
key={round.id}
className="tournament-bracket__elim__column"
style={
{
"--brackets-bottom-border-length":
round.matches.length === 1 ? 0 : undefined,
"--brackets-column-matches":
round.matches.length === 1 ? 0 : round.matches.length,
} as any
}
>
<div className="tournament-bracket__elim__matches">
{round.matches.map((match) => {
return <Match key={match.id} match={match} />;
})}
</div>
<div className="tournament-bracket__elim__lines">
{i !== bracketSide.length - 1 &&
new Array(Math.ceil(round.matches.length / 2))
.fill(null)
.map((_, i) => <div key={i} />)}
</div>
</div>
);
})}
</div>
</div>
);
}
function RoundInfo(props: {
round: {
title: string;
bestOf: number;
status: "DONE" | "INPROGRESS" | "UPCOMING";
};
function RoundInfo({
title,
bestOf,
status,
isLast,
}: {
title: string;
bestOf: number;
status: "DONE" | "INPROGRESS" | "UPCOMING";
isLast?: boolean;
}) {
return (
<div
className={classNames("tournament-bracket__elim__roundInfo", {
highlighted: props.round.status === "INPROGRESS",
last: props.isLast,
highlighted: status === "INPROGRESS",
last: isLast,
})}
>
<div className="tournament-bracket__elim__roundTitle">
{props.round.title}
</div>
{props.round.status !== "DONE" && (
<div className="tournament-bracket__elim__bestOf">
Bo{props.round.bestOf}
</div>
<div className="tournament-bracket__elim__roundTitle">{title}</div>
{status !== "DONE" && (
<div className="tournament-bracket__elim__bestOf">Bo{bestOf}</div>
)}
</div>
);
}
export function Match() {
export function Match({
match,
}: {
match: Unpacked<Unpacked<BracketModified["winners"]>["matches"]>;
}) {
return (
<div className="tournament-bracket__elim__match">
<div className="tournament-bracket__elim__roundNumber">1</div>
@@ -130,7 +100,14 @@ export function Match() {
"tournament-bracket__elim__teamOne"
)}
>
Team Olive <span className="tournament-bracket__elim__score">1</span>
{match.participants?.[0]}{" "}
<span
className={classNames("tournament-bracket__elim__score", {
"visibility-hidden": typeof match.score?.[0] !== "number",
})}
>
{match.score?.[0] ?? 0}
</span>
</div>
<div
className={classNames(
@@ -138,7 +115,14 @@ export function Match() {
"tournament-bracket__elim__teamTwo"
)}
>
Chimera <span className="tournament-bracket__elim__score">1</span>
{match.participants?.[1]}{" "}
<span
className={classNames("tournament-bracket__elim__score", {
"visibility-hidden": typeof match.score?.[0] !== "number",
})}
>
{match.score?.[1] ?? 0}
</span>
</div>
</div>
);

View File

@@ -89,26 +89,33 @@ export function getRoundsDefaultBestOf(
};
}
export function winnersRoundNames(count: number, isSE: boolean) {
return new Array(count).fill(null).map((_, i) => {
if (i === count - 4 + Number(isSE)) {
return "Winners' Semifinals";
}
if (i === count - 3 + Number(isSE)) return "Winners' Finals";
if (i === count - 2 + Number(isSE)) return "Grand Finals";
if (!isSE && i === count - 1) return "Bracket Reset";
return `Winners' Round ${i + 1}`;
});
}
export function losersRoundNames(count: number) {
return new Array(count)
.fill(null)
.map((_, i) =>
i === count - 1 ? "Losers' Finals" : `Losers' Round ${i + 1}`
);
}
export function getRoundNames(bracket: Bracket): EliminationBracket<string[]> {
const { winners: winnersRoundCount, losers: losersRoundCount } =
countRounds(bracket);
return {
winners: new Array(winnersRoundCount).fill(null).map((_, i) => {
const isSE = bracket.losers.length === 0;
if (i === winnersRoundCount - 4 + Number(isSE)) {
return "Winners' Semifinals";
}
if (i === winnersRoundCount - 3 + Number(isSE)) return "Winners' Finals";
if (i === winnersRoundCount - 2 + Number(isSE)) return "Grand Finals";
if (!isSE && i === winnersRoundCount - 1) return "Bracket Reset";
return `Winners' Round ${i + 1}`;
}),
losers: new Array(losersRoundCount)
.fill(null)
.map((_, i) =>
i === losersRoundCount - 1 ? "Losers' Finals" : `Losers' Round ${i + 1}`
),
winners: winnersRoundNames(winnersRoundCount, losersRoundCount === 0),
losers: losersRoundNames(losersRoundCount),
};
}

View File

@@ -8,6 +8,7 @@ export function findById(bracketId: string) {
select: {
rounds: {
select: {
id: true,
position: true,
_count: {
select: {
@@ -16,6 +17,7 @@ export function findById(bracketId: string) {
},
matches: {
select: {
id: true,
participants: {
select: {
team: {

View File

@@ -64,6 +64,7 @@ export default function TournamentPage() {
const tournamentIsOver = false;
if (tournamentHasStarted(data.brackets)) {
// TODO: link broken
result.push({ code: "bracket", text: "Bracket" });
if (!tournamentIsOver) {
result.push({ code: "streams", text: "Streams (4)" });

View File

@@ -19,14 +19,17 @@ export const loader: LoaderFunction = async ({ params }) => {
return typedJson(bracket);
};
// TODO: top seed is at the bottom of the tournament
// TODO: bottom bracket lines are wrong
// TODO: match numbers are off
// TODO: make bracket a bit smaller
export default function BracketTabWrapper() {
const data = useLoaderData<BracketModified>();
console.log("data", data);
return (
<div className="tournament-bracket__container">
<EliminationBracket />
<EliminationBracket bracketSide={data.winners} />
<EliminationBracket bracketSide={data.losers} />
</div>
);
}

View File

@@ -5,16 +5,19 @@ import {
} from "~/constants";
import {
EliminationBracket,
EliminationBracketSide,
losersRoundNames,
MapListIds,
tournamentRoundsForDB,
winnersRoundNames,
} from "~/core/tournament/bracket";
import { isTournamentAdmin } from "~/core/tournament/permissions";
import { sortTeamsBySeed } from "~/core/tournament/utils";
import * as Tournament from "~/models/Tournament";
import * as TournamentBracket from "~/models/TournamentBracket";
import * as TournamentTeam from "~/models/TournamentTeam";
import * as TournamentTeamMember from "~/models/TournamentTeamMember";
import * as TrustRelationship from "~/models/TrustRelationship";
import * as TournamentBracket from "~/models/TournamentBracket";
import { Serialized, Unpacked } from "~/utils";
import { db } from "~/utils/db.server";
@@ -127,8 +130,11 @@ export async function findTournamentWithInviteCodes({
export type BracketModified = EliminationBracket<BracketModifiedSide>;
type BracketModifiedSide = {
id: string;
name: string;
bestOf: number;
matches: {
id: string;
score?: [upperTeamScore: number, lowerTeamScore: number];
participants?: [upperTeamName: string | null, lowerTeamName: string | null];
}[];
@@ -139,29 +145,39 @@ export async function bracketById(bracketId: string): Promise<BracketModified> {
if (!bracket) throw new Response("No bracket found", { status: 404 });
const winnersRounds = bracket.rounds
.filter((round) => round.position > 0)
.sort((a, b) => a.position - b.position);
const losersRounds = bracket.rounds
.filter((round) => round.position < 0)
.sort((a, b) => b.position - a.position);
console.log("losersRounds.length", losersRounds.length);
return {
winners: modifyRounds(
bracket.rounds
.filter((round) => round.position > 0)
.sort((a, b) => a.position - b.position)
),
losers: modifyRounds(
bracket.rounds
.filter((round) => round.position < 0)
.sort((a, b) => b.position - a.position)
),
winners: modifyRounds(winnersRounds, "winners", losersRounds.length === 0),
losers: modifyRounds(losersRounds, "losers", false),
};
}
function modifyRounds(
rounds: NonNullable<TournamentBracket.FindById>["rounds"]
rounds: NonNullable<TournamentBracket.FindById>["rounds"],
side: EliminationBracketSide,
isSE: boolean
): BracketModifiedSide {
return rounds.map((round) => {
const roundNames =
side === "winners"
? winnersRoundNames(rounds.length, isSE)
: losersRoundNames(rounds.length);
return rounds.map((round, i) => {
return {
id: round.id,
name: roundNames[i],
bestOf: round._count.stages,
matches: round.matches.map((match) => {
const score =
match.results.length > 0
match.participants.length === 2
? match.results.reduce(
(scores: [number, number], result) => {
if (result.winner === "UPPER") scores[0]++;
@@ -182,6 +198,7 @@ function modifyRounds(
]
: undefined;
return {
id: match.id,
score,
participants,
};

View File

@@ -1,6 +1,9 @@
/* TODO: should be able to go wider than container https://stackoverflow.com/questions/5581034/is-there-are-way-to-make-a-child-divs-width-wider-than-the-parent-div-using-css */
.tournament-bracket__container {
display: flex;
width: 100%;
flex-direction: column;
gap: var(--s-6);
}
.tournament-bracket__elim__container {
@@ -95,7 +98,7 @@
.tournament-bracket__elim__matches {
display: flex;
height: calc(var(--brackets-match-height) * var(--brackets-matches));
height: calc(var(--brackets-match-height) * var(--brackets-max-matches));
flex-direction: column;
justify-content: space-around;
}
@@ -111,7 +114,7 @@
width: calc(var(--brackets-match-height) / 4);
height: calc(
2px + var(--brackets-match-height) *
(var(--brackets-matches) / var(--brackets-column-matches))
(var(--brackets-max-matches) / var(--brackets-column-matches))
);
border-top: 2px solid var(--theme-transparent);
border-bottom: var(--brackets-bottom-border-length, 2px) solid