mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-04-29 02:37:07 -05:00
Override tournament bracket destination (#1985)
* Initial * Progress * Done? * Update seeding nth
This commit is contained in:
parent
d34782e720
commit
d3a825bd57
|
|
@ -57,6 +57,7 @@ export function Button(props: ButtonProps) {
|
||||||
{icon &&
|
{icon &&
|
||||||
React.cloneElement(icon, {
|
React.cloneElement(icon, {
|
||||||
className: clsx("button-icon", { lonely: !children }),
|
className: clsx("button-icon", { lonely: !children }),
|
||||||
|
title: rest.title,
|
||||||
})}
|
})}
|
||||||
{loading && loadingText ? loadingText : children}
|
{loading && loadingText ? loadingText : children}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
export function EditIcon({ className }: { className?: string }) {
|
export function EditIcon({
|
||||||
|
className,
|
||||||
|
title,
|
||||||
|
}: { className?: string; title?: string }) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
@ -6,7 +9,7 @@ export function EditIcon({ className }: { className?: string }) {
|
||||||
viewBox="0 0 20 20"
|
viewBox="0 0 20 20"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
>
|
>
|
||||||
<title>Edit Icon</title>
|
<title>{title ?? "Edit Icon"}</title>
|
||||||
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z" />
|
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z" />
|
||||||
<path
|
<path
|
||||||
fillRule="evenodd"
|
fillRule="evenodd"
|
||||||
|
|
|
||||||
|
|
@ -688,6 +688,13 @@ export interface TournamentOrganizationSeries {
|
||||||
showLeaderboard: Generated<number>;
|
showLeaderboard: Generated<number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TournamentBracketProgressionOverride {
|
||||||
|
sourceBracketIdx: number;
|
||||||
|
destinationBracketIdx: number;
|
||||||
|
tournamentTeamId: number;
|
||||||
|
tournamentId: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TrustRelationship {
|
export interface TrustRelationship {
|
||||||
trustGiverUserId: number;
|
trustGiverUserId: number;
|
||||||
trustReceiverUserId: number;
|
trustReceiverUserId: number;
|
||||||
|
|
@ -923,6 +930,7 @@ export interface DB {
|
||||||
TournamentOrganizationMember: TournamentOrganizationMember;
|
TournamentOrganizationMember: TournamentOrganizationMember;
|
||||||
TournamentOrganizationBadge: TournamentOrganizationBadge;
|
TournamentOrganizationBadge: TournamentOrganizationBadge;
|
||||||
TournamentOrganizationSeries: TournamentOrganizationSeries;
|
TournamentOrganizationSeries: TournamentOrganizationSeries;
|
||||||
|
TournamentBracketProgressionOverride: TournamentBracketProgressionOverride;
|
||||||
TrustRelationship: TrustRelationship;
|
TrustRelationship: TrustRelationship;
|
||||||
UnvalidatedUserSubmittedImage: UnvalidatedUserSubmittedImage;
|
UnvalidatedUserSubmittedImage: UnvalidatedUserSubmittedImage;
|
||||||
UnvalidatedVideo: UnvalidatedVideo;
|
UnvalidatedVideo: UnvalidatedVideo;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,352 @@
|
||||||
|
import { Link, useFetcher } from "@remix-run/react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import * as React from "react";
|
||||||
|
import { Button } from "../../../../components/Button";
|
||||||
|
import { CheckmarkIcon } from "../../../../components/icons/Checkmark";
|
||||||
|
import { CrossIcon } from "../../../../components/icons/Cross";
|
||||||
|
import { EditIcon } from "../../../../components/icons/Edit";
|
||||||
|
import { logger } from "../../../../utils/logger";
|
||||||
|
import { tournamentTeamPage } from "../../../../utils/urls";
|
||||||
|
import { useUser } from "../../../auth/core/user";
|
||||||
|
import type { Bracket } from "../../core/Bracket";
|
||||||
|
import * as Progression from "../../core/Progression";
|
||||||
|
|
||||||
|
export function PlacementsTable({
|
||||||
|
groupId,
|
||||||
|
bracket,
|
||||||
|
allMatchesFinished,
|
||||||
|
}: {
|
||||||
|
groupId: number;
|
||||||
|
bracket: Bracket;
|
||||||
|
allMatchesFinished: boolean;
|
||||||
|
}) {
|
||||||
|
const user = useUser();
|
||||||
|
|
||||||
|
const _standings = bracket
|
||||||
|
.currentStandings(true)
|
||||||
|
.filter((s) => s.groupId === groupId);
|
||||||
|
|
||||||
|
const missingTeams = bracket.data.match.reduce((acc, cur) => {
|
||||||
|
if (cur.group_id !== groupId) return acc;
|
||||||
|
|
||||||
|
if (
|
||||||
|
cur.opponent1?.id &&
|
||||||
|
!_standings.some((s) => s.team.id === cur.opponent1!.id) &&
|
||||||
|
!acc.includes(cur.opponent1.id)
|
||||||
|
) {
|
||||||
|
acc.push(cur.opponent1.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
cur.opponent2?.id &&
|
||||||
|
!_standings.some((s) => s.team.id === cur.opponent2!.id) &&
|
||||||
|
!acc.includes(cur.opponent2.id)
|
||||||
|
) {
|
||||||
|
acc.push(cur.opponent2.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, [] as number[]);
|
||||||
|
|
||||||
|
const standings = _standings
|
||||||
|
.concat(
|
||||||
|
missingTeams.map((id) => ({
|
||||||
|
team: bracket.tournament.teamById(id)!,
|
||||||
|
stats: {
|
||||||
|
mapLosses: 0,
|
||||||
|
mapWins: 0,
|
||||||
|
points: 0,
|
||||||
|
setLosses: 0,
|
||||||
|
setWins: 0,
|
||||||
|
winsAgainstTied: 0,
|
||||||
|
lossesAgainstTied: 0,
|
||||||
|
},
|
||||||
|
placement: Math.max(..._standings.map((s) => s.placement)) + 1,
|
||||||
|
groupId,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a.placement === b.placement && a.team.seed && b.team.seed) {
|
||||||
|
return a.team.seed - b.team.seed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.placement - b.placement;
|
||||||
|
});
|
||||||
|
|
||||||
|
const destinationBracket = (placement: number) =>
|
||||||
|
bracket.tournament.brackets.find(
|
||||||
|
(b) =>
|
||||||
|
b.id !== bracket.id &&
|
||||||
|
b.sources?.some(
|
||||||
|
(s) => s.bracketIdx === 0 && s.placements.includes(placement),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const possibleDestinationBrackets = Progression.destinationsFromBracketIdx(
|
||||||
|
bracket.idx,
|
||||||
|
bracket.tournament.ctx.settings.bracketProgression,
|
||||||
|
).map((idx) => bracket.tournament.bracketByIdx(idx)!);
|
||||||
|
const canEditDestination = (() => {
|
||||||
|
const allDestinationsPreview = possibleDestinationBrackets.every(
|
||||||
|
(b) => b.preview,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
bracket.tournament.isOrganizer(user) &&
|
||||||
|
allDestinationsPreview &&
|
||||||
|
allMatchesFinished
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table className="rr__placements-table" cellSpacing={0}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Team</th>
|
||||||
|
<th>
|
||||||
|
<abbr title="Set wins and losses">W/L</abbr>
|
||||||
|
</th>
|
||||||
|
{bracket.type === "round_robin" ? (
|
||||||
|
<th>
|
||||||
|
<abbr title="Wins against tied opponents">TB</abbr>
|
||||||
|
</th>
|
||||||
|
) : null}
|
||||||
|
{bracket.type === "swiss" ? (
|
||||||
|
<th>
|
||||||
|
<abbr title="Losses against tied opponents">TB</abbr>
|
||||||
|
</th>
|
||||||
|
) : null}
|
||||||
|
<th>
|
||||||
|
<abbr title="Map wins and losses">W/L (M)</abbr>
|
||||||
|
</th>
|
||||||
|
{bracket.type === "round_robin" ? (
|
||||||
|
<th>
|
||||||
|
<abbr title="Score summed up">Scr</abbr>
|
||||||
|
</th>
|
||||||
|
) : null}
|
||||||
|
{bracket.type === "swiss" ? (
|
||||||
|
<>
|
||||||
|
<th>
|
||||||
|
<abbr title="Buchholz (summed set wins of opponents)">
|
||||||
|
Buch.
|
||||||
|
</abbr>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<abbr title="Buchholz (summed map wins of opponents)">
|
||||||
|
Buch. (M)
|
||||||
|
</abbr>
|
||||||
|
</th>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
<th>Seed</th>
|
||||||
|
<th />
|
||||||
|
{canEditDestination ? <th /> : null}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{standings.map((s, i) => {
|
||||||
|
const stats = s.stats!;
|
||||||
|
if (!stats) {
|
||||||
|
logger.error("No stats for team", s.team);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const team = bracket.tournament.teamById(s.team.id);
|
||||||
|
|
||||||
|
const dest = destinationBracket(i + 1);
|
||||||
|
|
||||||
|
const overridenDestination =
|
||||||
|
bracket.tournament.ctx.bracketProgressionOverrides.find(
|
||||||
|
(override) =>
|
||||||
|
override.sourceBracketIdx === bracket.idx &&
|
||||||
|
override.tournamentTeamId === s.team.id,
|
||||||
|
);
|
||||||
|
const overridenDestinationBracket = overridenDestination
|
||||||
|
? bracket.tournament.bracketByIdx(
|
||||||
|
overridenDestination.destinationBracketIdx,
|
||||||
|
)!
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={s.team.id}>
|
||||||
|
<td>
|
||||||
|
<Link
|
||||||
|
to={tournamentTeamPage({
|
||||||
|
tournamentId: bracket.tournament.ctx.id,
|
||||||
|
tournamentTeamId: s.team.id,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{s.team.name}{" "}
|
||||||
|
</Link>
|
||||||
|
{s.team.droppedOut ? (
|
||||||
|
<span className="text-warning text-xxxs font-bold">
|
||||||
|
Drop-out
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span>
|
||||||
|
{stats.setWins}/{stats.setLosses}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
{bracket.type === "round_robin" ? (
|
||||||
|
<td>
|
||||||
|
<span>{stats.winsAgainstTied}</span>
|
||||||
|
</td>
|
||||||
|
) : null}
|
||||||
|
{bracket.type === "swiss" ? (
|
||||||
|
<td>
|
||||||
|
<span>{(stats.lossesAgainstTied ?? 0) * -1}</span>
|
||||||
|
</td>
|
||||||
|
) : null}
|
||||||
|
<td>
|
||||||
|
<span>
|
||||||
|
{stats.mapWins}/{stats.mapLosses}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
{bracket.type === "round_robin" ? (
|
||||||
|
<td>
|
||||||
|
<span>{stats.points}</span>
|
||||||
|
</td>
|
||||||
|
) : null}
|
||||||
|
{bracket.type === "swiss" ? (
|
||||||
|
<>
|
||||||
|
<td>
|
||||||
|
<span>{stats.buchholzSets}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span>{stats.buchholzMaps}</span>
|
||||||
|
</td>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
<td>{team?.seed}</td>
|
||||||
|
<EditableDestination
|
||||||
|
key={overridenDestinationBracket?.idx}
|
||||||
|
source={bracket}
|
||||||
|
destination={dest}
|
||||||
|
overridenDestination={overridenDestinationBracket}
|
||||||
|
possibleDestinations={possibleDestinationBrackets}
|
||||||
|
allMatchesFinished={allMatchesFinished}
|
||||||
|
canEditDestination={canEditDestination}
|
||||||
|
tournamentTeamId={s.team.id}
|
||||||
|
/>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditableDestination({
|
||||||
|
source,
|
||||||
|
destination,
|
||||||
|
overridenDestination,
|
||||||
|
possibleDestinations: _possibleDestinations,
|
||||||
|
allMatchesFinished,
|
||||||
|
canEditDestination,
|
||||||
|
tournamentTeamId,
|
||||||
|
}: {
|
||||||
|
source: Bracket;
|
||||||
|
destination?: Bracket;
|
||||||
|
overridenDestination?: Bracket;
|
||||||
|
possibleDestinations: Bracket[];
|
||||||
|
allMatchesFinished: boolean;
|
||||||
|
canEditDestination: boolean;
|
||||||
|
tournamentTeamId: number;
|
||||||
|
}) {
|
||||||
|
const fetcher = useFetcher<any>();
|
||||||
|
const [editingDestination, setEditingDestination] = React.useState(false);
|
||||||
|
const [newDestinationIdx, setNewDestinationIdx] = React.useState<
|
||||||
|
number | null
|
||||||
|
>(overridenDestination?.idx ?? destination?.idx ?? -1);
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
fetcher.submit(
|
||||||
|
{
|
||||||
|
_action: "OVERRIDE_BRACKET_PROGRESSION",
|
||||||
|
tournamentTeamId,
|
||||||
|
sourceBracketIdx: source.idx,
|
||||||
|
destinationBracketIdx: newDestinationIdx,
|
||||||
|
},
|
||||||
|
{ method: "post", encType: "application/json" },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const possibleDestinations = !destination
|
||||||
|
? (["ELIMINATED", ..._possibleDestinations] as const)
|
||||||
|
: _possibleDestinations;
|
||||||
|
|
||||||
|
if (editingDestination) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<td>
|
||||||
|
<select
|
||||||
|
value={String(newDestinationIdx)}
|
||||||
|
onChange={(e) => setNewDestinationIdx(Number(e.target.value))}
|
||||||
|
>
|
||||||
|
{possibleDestinations.map((b) => (
|
||||||
|
<option
|
||||||
|
key={b === "ELIMINATED" ? "ELIMINATED" : b.id}
|
||||||
|
value={b === "ELIMINATED" ? -1 : b.idx}
|
||||||
|
>
|
||||||
|
{b === "ELIMINATED" ? "Eliminated" : b.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="stack horizontal xs">
|
||||||
|
<Button
|
||||||
|
variant="minimal"
|
||||||
|
title="Save destination"
|
||||||
|
icon={<CheckmarkIcon />}
|
||||||
|
size="tiny"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="minimal-destructive"
|
||||||
|
title="Cancel"
|
||||||
|
size="tiny"
|
||||||
|
icon={<CrossIcon />}
|
||||||
|
onClick={() => setEditingDestination(false)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{allMatchesFinished &&
|
||||||
|
overridenDestination &&
|
||||||
|
overridenDestination.idx !== destination?.idx ? (
|
||||||
|
<td className="text-theme font-bold">
|
||||||
|
<span>→ {overridenDestination.name}</span>
|
||||||
|
</td>
|
||||||
|
) : destination ? (
|
||||||
|
<td
|
||||||
|
className={clsx({
|
||||||
|
"italic text-lighter": !allMatchesFinished,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<span>→ {destination.name}</span>
|
||||||
|
</td>
|
||||||
|
) : (
|
||||||
|
<td />
|
||||||
|
)}
|
||||||
|
{canEditDestination ? (
|
||||||
|
<td>
|
||||||
|
<Button
|
||||||
|
variant="minimal"
|
||||||
|
title="Edit destination"
|
||||||
|
icon={<EditIcon />}
|
||||||
|
size="tiny"
|
||||||
|
onClick={() => setEditingDestination(true)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
import { Link } from "@remix-run/react";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import type { Match as MatchType } from "~/modules/brackets-model";
|
import type { Match as MatchType } from "~/modules/brackets-model";
|
||||||
import { logger } from "~/utils/logger";
|
|
||||||
import { tournamentTeamPage } from "~/utils/urls";
|
|
||||||
import type { Bracket as BracketType } from "../../core/Bracket";
|
import type { Bracket as BracketType } from "../../core/Bracket";
|
||||||
import { groupNumberToLetter } from "../../tournament-bracket-utils";
|
import { groupNumberToLetter } from "../../tournament-bracket-utils";
|
||||||
import { Match } from "./Match";
|
import { Match } from "./Match";
|
||||||
|
import { PlacementsTable } from "./PlacementsTable";
|
||||||
import { RoundHeader } from "./RoundHeader";
|
import { RoundHeader } from "./RoundHeader";
|
||||||
|
|
||||||
export function RoundRobinBracket({ bracket }: { bracket: BracketType }) {
|
export function RoundRobinBracket({ bracket }: { bracket: BracketType }) {
|
||||||
|
|
@ -118,152 +115,3 @@ function getGroups(bracket: BracketType) {
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PlacementsTable({
|
|
||||||
groupId,
|
|
||||||
bracket,
|
|
||||||
allMatchesFinished,
|
|
||||||
}: {
|
|
||||||
groupId: number;
|
|
||||||
bracket: BracketType;
|
|
||||||
allMatchesFinished: boolean;
|
|
||||||
}) {
|
|
||||||
const _standings = bracket
|
|
||||||
.currentStandings(true)
|
|
||||||
.filter((s) => s.groupId === groupId);
|
|
||||||
|
|
||||||
const missingTeams = bracket.data.match.reduce((acc, cur) => {
|
|
||||||
if (cur.group_id !== groupId) return acc;
|
|
||||||
|
|
||||||
if (
|
|
||||||
cur.opponent1?.id &&
|
|
||||||
!_standings.some((s) => s.team.id === cur.opponent1!.id) &&
|
|
||||||
!acc.includes(cur.opponent1.id)
|
|
||||||
) {
|
|
||||||
acc.push(cur.opponent1.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
cur.opponent2?.id &&
|
|
||||||
!_standings.some((s) => s.team.id === cur.opponent2!.id) &&
|
|
||||||
!acc.includes(cur.opponent2.id)
|
|
||||||
) {
|
|
||||||
acc.push(cur.opponent2.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, [] as number[]);
|
|
||||||
|
|
||||||
const standings = _standings
|
|
||||||
.concat(
|
|
||||||
missingTeams.map((id) => ({
|
|
||||||
team: bracket.tournament.teamById(id)!,
|
|
||||||
stats: {
|
|
||||||
mapLosses: 0,
|
|
||||||
mapWins: 0,
|
|
||||||
points: 0,
|
|
||||||
setLosses: 0,
|
|
||||||
setWins: 0,
|
|
||||||
winsAgainstTied: 0,
|
|
||||||
},
|
|
||||||
placement: Math.max(..._standings.map((s) => s.placement)) + 1,
|
|
||||||
groupId,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.sort((a, b) => {
|
|
||||||
if (a.placement === b.placement && a.team.seed && b.team.seed) {
|
|
||||||
return a.team.seed - b.team.seed;
|
|
||||||
}
|
|
||||||
|
|
||||||
return a.placement - b.placement;
|
|
||||||
});
|
|
||||||
|
|
||||||
const destinationBracket = (placement: number) =>
|
|
||||||
bracket.tournament.brackets.find(
|
|
||||||
(b) =>
|
|
||||||
b.id !== bracket.id &&
|
|
||||||
b.sources?.some(
|
|
||||||
(s) => s.bracketIdx === 0 && s.placements.includes(placement),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<table className="rr__placements-table" cellSpacing={0}>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Team</th>
|
|
||||||
<th>
|
|
||||||
<abbr title="Set wins and losses">W/L</abbr>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
<abbr title="Wins against tied opponents">TB</abbr>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
<abbr title="Map wins and losses">W/L (M)</abbr>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
<abbr title="Score summed up">Scr</abbr>
|
|
||||||
</th>
|
|
||||||
<th>Seed</th>
|
|
||||||
<th />
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{standings.map((s, i) => {
|
|
||||||
const stats = s.stats!;
|
|
||||||
if (!stats) {
|
|
||||||
logger.error("No stats for team", s.team);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const team = bracket.tournament.teamById(s.team.id);
|
|
||||||
|
|
||||||
const dest = destinationBracket(i + 1);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr key={s.team.id}>
|
|
||||||
<td>
|
|
||||||
<Link
|
|
||||||
to={tournamentTeamPage({
|
|
||||||
tournamentId: bracket.tournament.ctx.id,
|
|
||||||
tournamentTeamId: s.team.id,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{s.team.name}
|
|
||||||
</Link>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span>
|
|
||||||
{stats.setWins}/{stats.setLosses}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span>{stats.winsAgainstTied}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span>
|
|
||||||
{stats.mapWins}/{stats.mapLosses}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span>{stats.points}</span>
|
|
||||||
</td>
|
|
||||||
<td>{team?.seed}</td>
|
|
||||||
{dest ? (
|
|
||||||
<td
|
|
||||||
className={clsx({
|
|
||||||
"italic text-lighter": !allMatchesFinished,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<span>→ {dest.name}</span>
|
|
||||||
</td>
|
|
||||||
) : (
|
|
||||||
<td />
|
|
||||||
)}
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Link, useFetcher } from "@remix-run/react";
|
import { useFetcher } from "@remix-run/react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { Button } from "~/components/Button";
|
import { Button } from "~/components/Button";
|
||||||
import { FormWithConfirm } from "~/components/FormWithConfirm";
|
import { FormWithConfirm } from "~/components/FormWithConfirm";
|
||||||
|
|
@ -10,11 +10,10 @@ import {
|
||||||
} from "~/features/tournament/routes/to.$id";
|
} from "~/features/tournament/routes/to.$id";
|
||||||
import { useSearchParamState } from "~/hooks/useSearchParamState";
|
import { useSearchParamState } from "~/hooks/useSearchParamState";
|
||||||
import type { Match as MatchType } from "~/modules/brackets-model";
|
import type { Match as MatchType } from "~/modules/brackets-model";
|
||||||
import { logger } from "~/utils/logger";
|
|
||||||
import { tournamentTeamPage } from "~/utils/urls";
|
|
||||||
import type { Bracket as BracketType } from "../../core/Bracket";
|
import type { Bracket as BracketType } from "../../core/Bracket";
|
||||||
import { groupNumberToLetter } from "../../tournament-bracket-utils";
|
import { groupNumberToLetter } from "../../tournament-bracket-utils";
|
||||||
import { Match } from "./Match";
|
import { Match } from "./Match";
|
||||||
|
import { PlacementsTable } from "./PlacementsTable";
|
||||||
import { RoundHeader } from "./RoundHeader";
|
import { RoundHeader } from "./RoundHeader";
|
||||||
|
|
||||||
export function SwissBracket({
|
export function SwissBracket({
|
||||||
|
|
@ -268,166 +267,3 @@ function getGroups(bracket: BracketType) {
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PlacementsTable({
|
|
||||||
groupId,
|
|
||||||
bracket,
|
|
||||||
allMatchesFinished,
|
|
||||||
}: {
|
|
||||||
groupId: number;
|
|
||||||
bracket: BracketType;
|
|
||||||
allMatchesFinished: boolean;
|
|
||||||
}) {
|
|
||||||
const _standings = bracket
|
|
||||||
.currentStandings(true)
|
|
||||||
.filter((s) => s.groupId === groupId);
|
|
||||||
|
|
||||||
const missingTeams = bracket.data.match.reduce((acc, cur) => {
|
|
||||||
if (cur.group_id !== groupId) return acc;
|
|
||||||
|
|
||||||
if (
|
|
||||||
cur.opponent1?.id &&
|
|
||||||
!_standings.some((s) => s.team.id === cur.opponent1!.id) &&
|
|
||||||
!acc.includes(cur.opponent1.id)
|
|
||||||
) {
|
|
||||||
acc.push(cur.opponent1.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
cur.opponent2?.id &&
|
|
||||||
!_standings.some((s) => s.team.id === cur.opponent2!.id) &&
|
|
||||||
!acc.includes(cur.opponent2.id)
|
|
||||||
) {
|
|
||||||
acc.push(cur.opponent2.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, [] as number[]);
|
|
||||||
|
|
||||||
const standings = _standings
|
|
||||||
.concat(
|
|
||||||
missingTeams.map((id) => ({
|
|
||||||
team: bracket.tournament.teamById(id)!,
|
|
||||||
stats: {
|
|
||||||
mapLosses: 0,
|
|
||||||
mapWins: 0,
|
|
||||||
points: 0,
|
|
||||||
setLosses: 0,
|
|
||||||
setWins: 0,
|
|
||||||
winsAgainstTied: 0,
|
|
||||||
lossesAgainstTied: 0,
|
|
||||||
},
|
|
||||||
placement: Math.max(..._standings.map((s) => s.placement)) + 1,
|
|
||||||
groupId,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.sort((a, b) => {
|
|
||||||
if (a.placement === b.placement && a.team.seed && b.team.seed) {
|
|
||||||
return a.team.seed - b.team.seed;
|
|
||||||
}
|
|
||||||
|
|
||||||
return a.placement - b.placement;
|
|
||||||
});
|
|
||||||
|
|
||||||
const destinationBracket = (placement: number) =>
|
|
||||||
bracket.tournament.brackets.find(
|
|
||||||
(b) =>
|
|
||||||
b.id !== bracket.id &&
|
|
||||||
b.sources?.some(
|
|
||||||
(s) => s.bracketIdx === 0 && s.placements.includes(placement),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<table className="rr__placements-table" cellSpacing={0}>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Team</th>
|
|
||||||
<th>
|
|
||||||
<abbr title="Set wins and losses">W/L</abbr>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
<abbr title="Losses against tied opponents">TB</abbr>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
<abbr title="Map wins and losses">W/L (M)</abbr>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
<abbr title="Buchholz (summed set wins of opponents)">Buch.</abbr>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
<abbr title="Buchholz (summed map wins of opponents)">
|
|
||||||
Buch. (M)
|
|
||||||
</abbr>
|
|
||||||
</th>
|
|
||||||
<th>Seed</th>
|
|
||||||
<th />
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{standings.map((s, i) => {
|
|
||||||
const stats = s.stats!;
|
|
||||||
if (!stats) {
|
|
||||||
logger.error("No stats for team", s.team);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const team = bracket.tournament.teamById(s.team.id);
|
|
||||||
|
|
||||||
const dest = destinationBracket(i + 1);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr key={s.team.id}>
|
|
||||||
<td>
|
|
||||||
<Link
|
|
||||||
to={tournamentTeamPage({
|
|
||||||
tournamentId: bracket.tournament.ctx.id,
|
|
||||||
tournamentTeamId: s.team.id,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{s.team.name}{" "}
|
|
||||||
</Link>
|
|
||||||
{s.team.droppedOut ? (
|
|
||||||
<span className="text-warning text-xxxs font-bold">
|
|
||||||
Drop-out
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span>
|
|
||||||
{stats.setWins}/{stats.setLosses}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span>{(stats.lossesAgainstTied ?? 0) * -1}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span>
|
|
||||||
{stats.mapWins}/{stats.mapLosses}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span>{stats.buchholzSets}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span>{stats.buchholzMaps}</span>
|
|
||||||
</td>
|
|
||||||
<td>{team?.seed}</td>
|
|
||||||
{dest ? (
|
|
||||||
<td
|
|
||||||
className={clsx({
|
|
||||||
"italic text-lighter": !allMatchesFinished,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<span>→ {dest.name}</span>
|
|
||||||
</td>
|
|
||||||
) : (
|
|
||||||
<td />
|
|
||||||
)}
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -580,3 +580,26 @@ describe("bracketIdxsForStandings", () => {
|
||||||
).toEqual([0]); // missing 1 because it's underground when DE is the source
|
).toEqual([0]); // missing 1 because it's underground when DE is the source
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("destinationsFromBracketIdx", () => {
|
||||||
|
it("returns correct destination (one destination)", () => {
|
||||||
|
expect(
|
||||||
|
Progression.destinationsFromBracketIdx(
|
||||||
|
0,
|
||||||
|
progressions.roundRobinToSingleElimination,
|
||||||
|
),
|
||||||
|
).toEqual([1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns correct destination (many destinations)", () => {
|
||||||
|
expect(
|
||||||
|
Progression.destinationsFromBracketIdx(0, progressions.lowInk),
|
||||||
|
).toEqual([1, 2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns an empty array if no destinations", () => {
|
||||||
|
expect(
|
||||||
|
Progression.destinationsFromBracketIdx(0, progressions.singleElimination),
|
||||||
|
).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -692,3 +692,22 @@ function bracketsReachableFrom(
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function destinationsFromBracketIdx(
|
||||||
|
sourceBracketIdx: number,
|
||||||
|
progression: ParsedBracket[],
|
||||||
|
): number[] {
|
||||||
|
const destinations: number[] = [];
|
||||||
|
|
||||||
|
for (const [destinationBracketIdx, bracket] of progression.entries()) {
|
||||||
|
if (!bracket.sources) continue;
|
||||||
|
|
||||||
|
for (const source of bracket.sources) {
|
||||||
|
if (source.bracketIdx === sourceBracketIdx) {
|
||||||
|
destinations.push(destinationBracketIdx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return destinations;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, it, test } from "vitest";
|
||||||
import type { Match } from "~/modules/brackets-model";
|
import type { Match } from "~/modules/brackets-model";
|
||||||
import { Tournament } from "./Tournament";
|
import { Tournament } from "./Tournament";
|
||||||
import {
|
import {
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
PADDLING_POOL_255_TOP_CUT_INITIAL_MATCHES,
|
PADDLING_POOL_255_TOP_CUT_INITIAL_MATCHES,
|
||||||
PADDLING_POOL_257,
|
PADDLING_POOL_257,
|
||||||
} from "./tests/mocks";
|
} from "./tests/mocks";
|
||||||
|
import { SWIM_OR_SINK_167 } from "./tests/mocks-sos";
|
||||||
|
|
||||||
describe("Follow-up bracket progression", () => {
|
describe("Follow-up bracket progression", () => {
|
||||||
const tournamentPP257 = new Tournament(PADDLING_POOL_257());
|
const tournamentPP257 = new Tournament(PADDLING_POOL_257());
|
||||||
|
|
@ -178,3 +179,179 @@ describe("Follow-up bracket progression", () => {
|
||||||
expect(different, "Amount of different matches is incorrect").toBe(2);
|
expect(different, "Amount of different matches is incorrect").toBe(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Bracket progression override", () => {
|
||||||
|
it("handles no override", () => {
|
||||||
|
const tournament = new Tournament({
|
||||||
|
...SWIM_OR_SINK_167(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(tournament.brackets[1].participantTournamentTeamIds).toHaveLength(
|
||||||
|
11,
|
||||||
|
);
|
||||||
|
expect(tournament.brackets[2].participantTournamentTeamIds).toHaveLength(
|
||||||
|
11,
|
||||||
|
);
|
||||||
|
expect(tournament.brackets[3].participantTournamentTeamIds).toHaveLength(
|
||||||
|
11,
|
||||||
|
);
|
||||||
|
expect(tournament.brackets[4].participantTournamentTeamIds).toHaveLength(
|
||||||
|
11,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("overrides causing the team to go to another bracket", () => {
|
||||||
|
const tournament = new Tournament({
|
||||||
|
...SWIM_OR_SINK_167([
|
||||||
|
{
|
||||||
|
tournamentTeamId: 14809,
|
||||||
|
destinationBracketIdx: 1,
|
||||||
|
sourceBracketIdx: 0,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
tournament.brackets[1].participantTournamentTeamIds.includes(14809),
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("overrides causing the team not to go to their original bracket", () => {
|
||||||
|
const tournament = new Tournament({
|
||||||
|
...SWIM_OR_SINK_167([
|
||||||
|
{
|
||||||
|
tournamentTeamId: 14809,
|
||||||
|
destinationBracketIdx: 1,
|
||||||
|
sourceBracketIdx: 0,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
tournament.brackets[2].participantTournamentTeamIds.includes(14809),
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores -1 override (used to indicate no progression)", () => {
|
||||||
|
const tournament = new Tournament({
|
||||||
|
...SWIM_OR_SINK_167([
|
||||||
|
{
|
||||||
|
tournamentTeamId: 14809,
|
||||||
|
destinationBracketIdx: -1,
|
||||||
|
sourceBracketIdx: 0,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(tournament.brackets[1].participantTournamentTeamIds).toHaveLength(
|
||||||
|
11,
|
||||||
|
);
|
||||||
|
expect(tournament.brackets[2].participantTournamentTeamIds).toHaveLength(
|
||||||
|
11,
|
||||||
|
);
|
||||||
|
expect(tournament.brackets[3].participantTournamentTeamIds).toHaveLength(
|
||||||
|
11,
|
||||||
|
);
|
||||||
|
expect(tournament.brackets[4].participantTournamentTeamIds).toHaveLength(
|
||||||
|
11,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("override teams seeded at the end", () => {
|
||||||
|
const tournament = new Tournament({
|
||||||
|
...SWIM_OR_SINK_167([
|
||||||
|
{
|
||||||
|
tournamentTeamId: 14809,
|
||||||
|
destinationBracketIdx: 1,
|
||||||
|
sourceBracketIdx: 0,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(tournament.brackets[1].seeding?.at(-1)).toBe(14809);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("if redundant override, still in the right bracket", () => {
|
||||||
|
const tournament = new Tournament({
|
||||||
|
...SWIM_OR_SINK_167([
|
||||||
|
{
|
||||||
|
tournamentTeamId: 14809,
|
||||||
|
destinationBracketIdx: 2,
|
||||||
|
sourceBracketIdx: 0,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
tournament.brackets[2].participantTournamentTeamIds.includes(14809),
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("redundants override does not affect the seed", () => {
|
||||||
|
const tournamentTeamId = 14735;
|
||||||
|
const tournament = new Tournament({
|
||||||
|
...SWIM_OR_SINK_167(),
|
||||||
|
});
|
||||||
|
const tournamentWOverride = new Tournament({
|
||||||
|
...SWIM_OR_SINK_167([
|
||||||
|
{
|
||||||
|
tournamentTeamId,
|
||||||
|
destinationBracketIdx: 2,
|
||||||
|
sourceBracketIdx: 0,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const seedingIdx =
|
||||||
|
tournament.brackets[2].seeding?.indexOf(tournamentTeamId);
|
||||||
|
const seedingIdxWOverride =
|
||||||
|
tournamentWOverride.brackets[2].seeding?.indexOf(tournamentTeamId);
|
||||||
|
|
||||||
|
expect(typeof seedingIdx === "number").toBeTruthy();
|
||||||
|
expect(seedingIdx).toBe(seedingIdxWOverride);
|
||||||
|
});
|
||||||
|
|
||||||
|
// note there is also logic for avoiding replays
|
||||||
|
it("override teams seeded according to their placement in the source bracket", () => {
|
||||||
|
const tournament = new Tournament({
|
||||||
|
...SWIM_OR_SINK_167([
|
||||||
|
// throw these to different brackets to avoid replays
|
||||||
|
{
|
||||||
|
tournamentTeamId: 14657,
|
||||||
|
destinationBracketIdx: 2,
|
||||||
|
sourceBracketIdx: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tournamentTeamId: 14800,
|
||||||
|
destinationBracketIdx: 2,
|
||||||
|
sourceBracketIdx: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tournamentTeamId: 14743,
|
||||||
|
destinationBracketIdx: 2,
|
||||||
|
sourceBracketIdx: 0,
|
||||||
|
},
|
||||||
|
// ---
|
||||||
|
{
|
||||||
|
tournamentTeamId: 14737,
|
||||||
|
destinationBracketIdx: 1,
|
||||||
|
sourceBracketIdx: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tournamentTeamId: 14809,
|
||||||
|
destinationBracketIdx: 1,
|
||||||
|
sourceBracketIdx: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tournamentTeamId: 14796,
|
||||||
|
destinationBracketIdx: 1,
|
||||||
|
sourceBracketIdx: 0,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(tournament.brackets[1].seeding?.at(-3)).toBe(14809);
|
||||||
|
expect(tournament.brackets[1].seeding?.at(-2)).toBe(14796);
|
||||||
|
expect(tournament.brackets[1].seeding?.at(-1)).toBe(14737);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,7 @@ export class Tournament {
|
||||||
);
|
);
|
||||||
} else if (type === "swiss") {
|
} else if (type === "swiss") {
|
||||||
const { teams, relevantMatchesFinished } = sources
|
const { teams, relevantMatchesFinished } = sources
|
||||||
? this.resolveTeamsFromSources(sources)
|
? this.resolveTeamsFromSources(sources, bracketIdx)
|
||||||
: {
|
: {
|
||||||
teams: this.ctx.teams.map((team) => team.id),
|
teams: this.ctx.teams.map((team) => team.id),
|
||||||
relevantMatchesFinished: true,
|
relevantMatchesFinished: true,
|
||||||
|
|
@ -209,7 +209,7 @@ export class Tournament {
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const { teams, relevantMatchesFinished } = sources
|
const { teams, relevantMatchesFinished } = sources
|
||||||
? this.resolveTeamsFromSources(sources)
|
? this.resolveTeamsFromSources(sources, bracketIdx)
|
||||||
: {
|
: {
|
||||||
teams: this.ctx.teams.map((team) => team.id),
|
teams: this.ctx.teams.map((team) => team.id),
|
||||||
relevantMatchesFinished: true,
|
relevantMatchesFinished: true,
|
||||||
|
|
@ -261,23 +261,75 @@ export class Tournament {
|
||||||
|
|
||||||
private resolveTeamsFromSources(
|
private resolveTeamsFromSources(
|
||||||
sources: NonNullable<Progression.ParsedBracket["sources"]>,
|
sources: NonNullable<Progression.ParsedBracket["sources"]>,
|
||||||
|
bracketIdx: number,
|
||||||
) {
|
) {
|
||||||
const teams: number[] = [];
|
const teams: number[] = [];
|
||||||
|
|
||||||
let allRelevantMatchesFinished = true;
|
let allRelevantMatchesFinished = true;
|
||||||
for (const { bracketIdx, placements } of sources) {
|
for (const source of sources) {
|
||||||
const sourceBracket = this.bracketByIdx(bracketIdx);
|
const sourceBracket = this.bracketByIdx(source.bracketIdx);
|
||||||
invariant(sourceBracket, "Bracket not found");
|
invariant(sourceBracket, "Bracket not found");
|
||||||
|
|
||||||
const { teams: sourcedTeams, relevantMatchesFinished } =
|
const { teams: sourcedTeams, relevantMatchesFinished } =
|
||||||
sourceBracket.source(placements);
|
sourceBracket.source(source.placements);
|
||||||
if (!relevantMatchesFinished) {
|
if (!relevantMatchesFinished) {
|
||||||
allRelevantMatchesFinished = false;
|
allRelevantMatchesFinished = false;
|
||||||
}
|
}
|
||||||
teams.push(...sourcedTeams);
|
|
||||||
|
const excludedOverridenTeams = sourcedTeams.filter(
|
||||||
|
(teamId) =>
|
||||||
|
!this.ctx.bracketProgressionOverrides.some(
|
||||||
|
(override) =>
|
||||||
|
override.sourceBracketIdx === source.bracketIdx &&
|
||||||
|
override.tournamentTeamId === teamId &&
|
||||||
|
// "no progression" override
|
||||||
|
override.destinationBracketIdx !== -1 &&
|
||||||
|
// redundant override
|
||||||
|
override.destinationBracketIdx !== bracketIdx,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
teams.push(...excludedOverridenTeams);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { teams, relevantMatchesFinished: allRelevantMatchesFinished };
|
const teamsFromOverride: { id: number; sourceBracketIdx: number }[] = [];
|
||||||
|
for (const source of sources) {
|
||||||
|
for (const override of this.ctx.bracketProgressionOverrides) {
|
||||||
|
if (override.sourceBracketIdx !== source.bracketIdx) continue;
|
||||||
|
if (override.destinationBracketIdx !== bracketIdx) continue;
|
||||||
|
|
||||||
|
teamsFromOverride.push({
|
||||||
|
id: override.tournamentTeamId,
|
||||||
|
sourceBracketIdx: source.bracketIdx,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const overridesWithoutRepeats = teamsFromOverride
|
||||||
|
.filter(({ id }) => !teams.includes(id))
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a.sourceBracketIdx !== b.sourceBracketIdx) return 0;
|
||||||
|
|
||||||
|
const bracket = this.bracketByIdx(a.sourceBracketIdx);
|
||||||
|
if (!bracket) return 0;
|
||||||
|
|
||||||
|
const aStanding = bracket.standings.find(
|
||||||
|
(standing) => standing.team.id === a.id,
|
||||||
|
);
|
||||||
|
const bStanding = bracket.standings.find(
|
||||||
|
(standing) => standing.team.id === b.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!aStanding || !bStanding) return 0;
|
||||||
|
|
||||||
|
return aStanding.placement - bStanding.placement;
|
||||||
|
})
|
||||||
|
.map(({ id }) => id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
teams: teams.concat(overridesWithoutRepeats),
|
||||||
|
relevantMatchesFinished: allRelevantMatchesFinished,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private avoidReplaysOfPreviousBracketOpponent(
|
private avoidReplaysOfPreviousBracketOpponent(
|
||||||
|
|
|
||||||
7630
app/features/tournament-bracket/core/tests/mocks-sos.ts
Normal file
7630
app/features/tournament-bracket/core/tests/mocks-sos.ts
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -1981,6 +1981,7 @@ export const PADDLING_POOL_257 = () =>
|
||||||
organization: null,
|
organization: null,
|
||||||
tags: null,
|
tags: null,
|
||||||
eventId: 1352,
|
eventId: 1352,
|
||||||
|
bracketProgressionOverrides: [],
|
||||||
settings: {
|
settings: {
|
||||||
bracketProgression: [
|
bracketProgression: [
|
||||||
{
|
{
|
||||||
|
|
@ -7882,6 +7883,7 @@ export const PADDLING_POOL_255 = () =>
|
||||||
organization: null,
|
organization: null,
|
||||||
tags: null,
|
tags: null,
|
||||||
eventId: 1286,
|
eventId: 1286,
|
||||||
|
bracketProgressionOverrides: [],
|
||||||
settings: {
|
settings: {
|
||||||
bracketProgression: [
|
bracketProgression: [
|
||||||
{
|
{
|
||||||
|
|
@ -14119,6 +14121,7 @@ export const IN_THE_ZONE_32 = ({
|
||||||
organization: null,
|
organization: null,
|
||||||
tags: null,
|
tags: null,
|
||||||
eventId: 1134,
|
eventId: 1134,
|
||||||
|
bracketProgressionOverrides: [],
|
||||||
settings: {
|
settings: {
|
||||||
bracketProgression: [
|
bracketProgression: [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ export const testTournament = ({
|
||||||
isFinalized: 0,
|
isFinalized: 0,
|
||||||
name: "test",
|
name: "test",
|
||||||
castTwitchAccounts: [],
|
castTwitchAccounts: [],
|
||||||
|
bracketProgressionOverrides: [],
|
||||||
subCounts: [],
|
subCounts: [],
|
||||||
staff: [],
|
staff: [],
|
||||||
tieBreakerMapPool: [],
|
tieBreakerMapPool: [],
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import {
|
||||||
import { currentSeason } from "~/features/mmr/season";
|
import { currentSeason } from "~/features/mmr/season";
|
||||||
import { refreshUserSkills } from "~/features/mmr/tiered.server";
|
import { refreshUserSkills } from "~/features/mmr/tiered.server";
|
||||||
import { TOURNAMENT, tournamentIdFromParams } from "~/features/tournament";
|
import { TOURNAMENT, tournamentIdFromParams } from "~/features/tournament";
|
||||||
|
import * as Progression from "~/features/tournament-bracket/core/Progression";
|
||||||
import * as TournamentRepository from "~/features/tournament/TournamentRepository.server";
|
import * as TournamentRepository from "~/features/tournament/TournamentRepository.server";
|
||||||
import { createSwissBracketInTransaction } from "~/features/tournament/queries/createSwissBracketInTransaction.server";
|
import { createSwissBracketInTransaction } from "~/features/tournament/queries/createSwissBracketInTransaction.server";
|
||||||
import { updateRoundMaps } from "~/features/tournament/queries/updateRoundMaps.server";
|
import { updateRoundMaps } from "~/features/tournament/queries/updateRoundMaps.server";
|
||||||
|
|
@ -173,6 +174,8 @@ export const action: ActionFunction = async ({ params, request }) => {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "ADVANCE_BRACKET": {
|
case "ADVANCE_BRACKET": {
|
||||||
|
validate(tournament.isOrganizer(user));
|
||||||
|
|
||||||
const bracket = tournament.bracketByIdx(data.bracketIdx);
|
const bracket = tournament.bracketByIdx(data.bracketIdx);
|
||||||
validate(bracket, "Bracket not found");
|
validate(bracket, "Bracket not found");
|
||||||
validate(bracket.type === "swiss", "Can't advance non-swiss bracket");
|
validate(bracket.type === "swiss", "Can't advance non-swiss bracket");
|
||||||
|
|
@ -187,6 +190,8 @@ export const action: ActionFunction = async ({ params, request }) => {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "UNADVANCE_BRACKET": {
|
case "UNADVANCE_BRACKET": {
|
||||||
|
validate(tournament.isOrganizer(user));
|
||||||
|
|
||||||
const bracket = tournament.bracketByIdx(data.bracketIdx);
|
const bracket = tournament.bracketByIdx(data.bracketIdx);
|
||||||
validate(bracket, "Bracket not found");
|
validate(bracket, "Bracket not found");
|
||||||
validate(bracket.type === "swiss", "Can't unadvance non-swiss bracket");
|
validate(bracket.type === "swiss", "Can't unadvance non-swiss bracket");
|
||||||
|
|
@ -267,6 +272,33 @@ export const action: ActionFunction = async ({ params, request }) => {
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "OVERRIDE_BRACKET_PROGRESSION": {
|
||||||
|
validate(tournament.isOrganizer(user));
|
||||||
|
|
||||||
|
const allDestinationBrackets = Progression.destinationsFromBracketIdx(
|
||||||
|
data.sourceBracketIdx,
|
||||||
|
tournament.ctx.settings.bracketProgression,
|
||||||
|
);
|
||||||
|
validate(
|
||||||
|
data.destinationBracketIdx === -1 ||
|
||||||
|
allDestinationBrackets.includes(data.destinationBracketIdx),
|
||||||
|
"Invalid destination bracket",
|
||||||
|
);
|
||||||
|
validate(
|
||||||
|
allDestinationBrackets.every(
|
||||||
|
(bracketIdx) => tournament.bracketByIdx(bracketIdx)!.preview,
|
||||||
|
),
|
||||||
|
"Can't override progression if follow-up brackets are started",
|
||||||
|
);
|
||||||
|
|
||||||
|
await TournamentRepository.overrideTeamBracketProgression({
|
||||||
|
tournamentTeamId: data.tournamentTeamId,
|
||||||
|
sourceBracketIdx: data.sourceBracketIdx,
|
||||||
|
destinationBracketIdx: data.destinationBracketIdx,
|
||||||
|
tournamentId,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
assertUnreachable(data);
|
assertUnreachable(data);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,12 @@ export const bracketSchema = z.union([
|
||||||
_action: _action("BRACKET_CHECK_IN"),
|
_action: _action("BRACKET_CHECK_IN"),
|
||||||
bracketIdx,
|
bracketIdx,
|
||||||
}),
|
}),
|
||||||
|
z.object({
|
||||||
|
_action: _action("OVERRIDE_BRACKET_PROGRESSION"),
|
||||||
|
tournamentTeamId: id,
|
||||||
|
sourceBracketIdx: bracketIdx,
|
||||||
|
destinationBracketIdx: z.union([bracketIdx, z.literal(-1)]),
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const matchPageParamsSchema = z.object({ mid: id });
|
export const matchPageParamsSchema = z.object({ mid: id });
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,20 @@ export async function findById(id: number) {
|
||||||
.where("TournamentSub.tournamentId", "=", id)
|
.where("TournamentSub.tournamentId", "=", id)
|
||||||
.groupBy("TournamentSub.visibility"),
|
.groupBy("TournamentSub.visibility"),
|
||||||
).as("subCounts"),
|
).as("subCounts"),
|
||||||
|
jsonArrayFrom(
|
||||||
|
eb
|
||||||
|
.selectFrom("TournamentBracketProgressionOverride")
|
||||||
|
.select([
|
||||||
|
"TournamentBracketProgressionOverride.sourceBracketIdx",
|
||||||
|
"TournamentBracketProgressionOverride.destinationBracketIdx",
|
||||||
|
"TournamentBracketProgressionOverride.tournamentTeamId",
|
||||||
|
])
|
||||||
|
.whereRef(
|
||||||
|
"TournamentBracketProgressionOverride.tournamentId",
|
||||||
|
"=",
|
||||||
|
"Tournament.id",
|
||||||
|
),
|
||||||
|
).as("bracketProgressionOverrides"),
|
||||||
exists(
|
exists(
|
||||||
selectFrom("TournamentResult")
|
selectFrom("TournamentResult")
|
||||||
.where("TournamentResult.tournamentId", "=", id)
|
.where("TournamentResult.tournamentId", "=", id)
|
||||||
|
|
@ -695,6 +709,29 @@ export function updateProgression({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function overrideTeamBracketProgression({
|
||||||
|
tournamentId,
|
||||||
|
tournamentTeamId,
|
||||||
|
sourceBracketIdx,
|
||||||
|
destinationBracketIdx,
|
||||||
|
}: {
|
||||||
|
tournamentId: number;
|
||||||
|
tournamentTeamId: number;
|
||||||
|
sourceBracketIdx: number;
|
||||||
|
destinationBracketIdx: number;
|
||||||
|
}) {
|
||||||
|
// set in migration: unique("sourceBracketIdx", "tournamentTeamId") on conflict replace
|
||||||
|
return db
|
||||||
|
.insertInto("TournamentBracketProgressionOverride")
|
||||||
|
.values({
|
||||||
|
tournamentId,
|
||||||
|
tournamentTeamId,
|
||||||
|
sourceBracketIdx,
|
||||||
|
destinationBracketIdx,
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
export function updateTeamName({
|
export function updateTeamName({
|
||||||
tournamentTeamId,
|
tournamentTeamId,
|
||||||
name,
|
name,
|
||||||
|
|
|
||||||
BIN
db-test.sqlite3
BIN
db-test.sqlite3
Binary file not shown.
21
migrations/076-progression-override.js
Normal file
21
migrations/076-progression-override.js
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
export function up(db) {
|
||||||
|
db.transaction(() => {
|
||||||
|
db.prepare(
|
||||||
|
/*sql*/ `
|
||||||
|
create table "TournamentBracketProgressionOverride" (
|
||||||
|
"sourceBracketIdx" integer not null,
|
||||||
|
"destinationBracketIdx" integer not null,
|
||||||
|
"tournamentTeamId" integer not null,
|
||||||
|
"tournamentId" integer not null,
|
||||||
|
unique("sourceBracketIdx", "tournamentTeamId") on conflict replace,
|
||||||
|
foreign key ("tournamentTeamId") references "TournamentTeam"("id") on delete cascade,
|
||||||
|
foreign key ("tournamentId") references "Tournament"("id") on delete cascade
|
||||||
|
) strict
|
||||||
|
`,
|
||||||
|
).run();
|
||||||
|
|
||||||
|
db.prepare(
|
||||||
|
/*sql*/ `create index tournament_bracket_progression_override_tournament_id on "TournamentBracketProgressionOverride"("tournamentId")`,
|
||||||
|
).run();
|
||||||
|
})();
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user