mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-27 21:55:15 -05:00
@@ -278,6 +278,7 @@ export const bracketProgressionSchema = z.preprocess(
|
||||
z.object({
|
||||
bracketIdx: z.number(),
|
||||
placements: z.array(z.number()),
|
||||
rest: z.boolean().optional(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
|
||||
@@ -577,40 +577,47 @@ function SourcesSelector({
|
||||
const inputBracket = brackets.find((b) => b.id === source?.bracketId);
|
||||
|
||||
return (
|
||||
<div className="stack horizontal sm items-end">
|
||||
<div>
|
||||
<Label htmlFor={createId("bracket")}>Bracket</Label>
|
||||
<select
|
||||
id={createId("bracket")}
|
||||
value={source?.bracketId ?? brackets[0].id}
|
||||
onChange={(e) =>
|
||||
onChange({ placements: "", ...source, bracketId: e.target.value })
|
||||
}
|
||||
>
|
||||
{brackets.map((bracket) => (
|
||||
<option key={bracket.id} value={bracket.id}>
|
||||
{bracket.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div>
|
||||
<div className="stack horizontal sm items-end">
|
||||
<div>
|
||||
<Label htmlFor={createId("bracket")}>Bracket</Label>
|
||||
<select
|
||||
id={createId("bracket")}
|
||||
value={source?.bracketId ?? brackets[0].id}
|
||||
onChange={(e) =>
|
||||
onChange({ placements: "", ...source, bracketId: e.target.value })
|
||||
}
|
||||
>
|
||||
{brackets.map((bracket) => (
|
||||
<option key={bracket.id} value={bracket.id}>
|
||||
{bracket.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{!inputBracket?.settings.advanceThreshold ? (
|
||||
<div>
|
||||
<Label htmlFor={createId("placements")}>Placements</Label>
|
||||
<Input
|
||||
id={createId("placements")}
|
||||
placeholder="1,2,3"
|
||||
value={source?.placements ?? ""}
|
||||
testId="placements-input"
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
bracketId: brackets[0].id,
|
||||
...source,
|
||||
placements: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{!inputBracket?.settings.advanceThreshold ? (
|
||||
<div>
|
||||
<Label htmlFor={createId("placements")}>Placements</Label>
|
||||
<Input
|
||||
id={createId("placements")}
|
||||
placeholder="1,2,3"
|
||||
value={source?.placements ?? ""}
|
||||
testId="placements-input"
|
||||
onChange={(e) =>
|
||||
onChange({
|
||||
bracketId: brackets[0].id,
|
||||
...source,
|
||||
placements: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<FormMessage type="info">
|
||||
Use N+ for Nth place and every placement after
|
||||
</FormMessage>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -424,7 +424,11 @@ export abstract class Bracket {
|
||||
return this.teamsPendingCheckIn.includes(team.id);
|
||||
}
|
||||
|
||||
source(_options: { placements: number[]; advanceThreshold?: number }): {
|
||||
source(_options: {
|
||||
placements: number[];
|
||||
advanceThreshold?: number;
|
||||
rest?: boolean;
|
||||
}): {
|
||||
relevantMatchesFinished: boolean;
|
||||
teams: number[];
|
||||
} {
|
||||
|
||||
@@ -12,7 +12,7 @@ export class RoundRobinBracket extends Bracket {
|
||||
return true;
|
||||
}
|
||||
|
||||
source({ placements }: { placements: number[] }): {
|
||||
source({ placements, rest }: { placements: number[]; rest?: boolean }): {
|
||||
relevantMatchesFinished: boolean;
|
||||
teams: number[];
|
||||
} {
|
||||
@@ -24,10 +24,18 @@ export class RoundRobinBracket extends Bracket {
|
||||
const relevantMatchesFinished =
|
||||
standings.length === this.participantTournamentTeamIds.length;
|
||||
|
||||
const maxExplicit = Math.max(...placements);
|
||||
const matchesPlacement = (p: number) =>
|
||||
placements.includes(p) || (rest === true && p >= maxExplicit);
|
||||
|
||||
if (this.settings?.hasAbDivisions) {
|
||||
return {
|
||||
relevantMatchesFinished,
|
||||
teams: this.teamsFromPlacementsPerAbDivision(standings, placements),
|
||||
teams: this.teamsFromPlacementsPerAbDivision(
|
||||
standings,
|
||||
placements,
|
||||
rest === true,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -41,7 +49,7 @@ export class RoundRobinBracket extends Bracket {
|
||||
return {
|
||||
relevantMatchesFinished,
|
||||
teams: standings
|
||||
.filter((s) => placements.includes(placementNormalized(s.placement)))
|
||||
.filter((s) => matchesPlacement(placementNormalized(s.placement)))
|
||||
.map((s) => s.team.id),
|
||||
};
|
||||
}
|
||||
@@ -49,19 +57,28 @@ export class RoundRobinBracket extends Bracket {
|
||||
private teamsFromPlacementsPerAbDivision(
|
||||
standings: Standing[],
|
||||
placements: number[],
|
||||
rest: boolean,
|
||||
): number[] {
|
||||
const groupIds = R.unique(
|
||||
standings
|
||||
.map((s) => s.groupId)
|
||||
.filter((id): id is number => typeof id === "number"),
|
||||
);
|
||||
const maxExplicit = Math.max(...placements);
|
||||
const teams: number[] = [];
|
||||
for (const groupId of groupIds) {
|
||||
for (const division of [0, 1] as const) {
|
||||
const divisionStandings = standings.filter(
|
||||
(s) => s.groupId === groupId && s.team.abDivision === division,
|
||||
);
|
||||
for (const placement of placements) {
|
||||
const maxPlacement = rest ? divisionStandings.length : maxExplicit;
|
||||
for (let placement = 1; placement <= maxPlacement; placement++) {
|
||||
if (
|
||||
!placements.includes(placement) &&
|
||||
!(rest && placement >= maxExplicit)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const standing = divisionStandings[placement - 1];
|
||||
if (standing) teams.push(standing.team.id);
|
||||
}
|
||||
|
||||
@@ -18,9 +18,11 @@ export class SwissBracket extends Bracket {
|
||||
source({
|
||||
placements,
|
||||
advanceThreshold,
|
||||
rest,
|
||||
}: {
|
||||
placements: number[];
|
||||
advanceThreshold?: number;
|
||||
rest?: boolean;
|
||||
}): {
|
||||
relevantMatchesFinished: boolean;
|
||||
teams: number[];
|
||||
@@ -84,10 +86,14 @@ export class SwissBracket extends Bracket {
|
||||
return uniquePlacements.indexOf(p) + 1;
|
||||
};
|
||||
|
||||
const maxExplicit = Math.max(...placements);
|
||||
const matchesPlacement = (p: number) =>
|
||||
placements.includes(p) || (rest === true && p >= maxExplicit);
|
||||
|
||||
return {
|
||||
relevantMatchesFinished,
|
||||
teams: standings
|
||||
.filter((s) => placements.includes(placementNormalized(s.placement)))
|
||||
.filter((s) => matchesPlacement(placementNormalized(s.placement)))
|
||||
.map((s) => s.team.id),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -258,6 +258,228 @@ describe("validatedSources - PLACEMENTS_PARSE_ERROR", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('validatedSources - rest "N+" syntax', () => {
|
||||
const getValidatedBracketsFromPlacements = (placements: string) => {
|
||||
return Progression.validatedBrackets([
|
||||
{
|
||||
id: "1",
|
||||
name: "Bracket 1",
|
||||
type: "round_robin",
|
||||
settings: { teamsPerGroup: 8 },
|
||||
requiresCheckIn: false,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Bracket 2",
|
||||
type: "single_elimination",
|
||||
settings: {},
|
||||
requiresCheckIn: false,
|
||||
sources: [
|
||||
{
|
||||
bracketId: "1",
|
||||
placements,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
it("parses lone rest placement", () => {
|
||||
const result = getValidatedBracketsFromPlacements(
|
||||
"5+",
|
||||
) as Progression.ParsedBracket[];
|
||||
|
||||
expect(result[1].sources).toEqual([
|
||||
{ bracketIdx: 0, placements: [5], rest: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it("parses rest from first placement", () => {
|
||||
const result = getValidatedBracketsFromPlacements(
|
||||
"1+",
|
||||
) as Progression.ParsedBracket[];
|
||||
|
||||
expect(result[1].sources).toEqual([
|
||||
{ bracketIdx: 0, placements: [1], rest: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it("parses rest combined with explicit placements", () => {
|
||||
const result = getValidatedBracketsFromPlacements(
|
||||
"1,2,3-4,5+",
|
||||
) as Progression.ParsedBracket[];
|
||||
|
||||
expect(result[1].sources).toEqual([
|
||||
{ bracketIdx: 0, placements: [1, 2, 3, 4, 5], rest: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it("parses range-then-rest as a single element", () => {
|
||||
const result = getValidatedBracketsFromPlacements(
|
||||
"1-5+",
|
||||
) as Progression.ParsedBracket[];
|
||||
|
||||
expect(result[1].sources).toEqual([
|
||||
{ bracketIdx: 0, placements: [1, 2, 3, 4, 5], rest: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects rest in non-final position", () => {
|
||||
const error = getValidatedBracketsFromPlacements(
|
||||
"5+,6",
|
||||
) as Progression.ValidationError;
|
||||
expect(error.type).toBe("PLACEMENTS_PARSE_ERROR");
|
||||
});
|
||||
|
||||
it("rejects double plus", () => {
|
||||
const error = getValidatedBracketsFromPlacements(
|
||||
"5++",
|
||||
) as Progression.ValidationError;
|
||||
expect(error.type).toBe("PLACEMENTS_PARSE_ERROR");
|
||||
});
|
||||
|
||||
it("rejects lone plus", () => {
|
||||
const error = getValidatedBracketsFromPlacements(
|
||||
"+",
|
||||
) as Progression.ValidationError;
|
||||
expect(error.type).toBe("PLACEMENTS_PARSE_ERROR");
|
||||
});
|
||||
|
||||
it("rejects rest on zero placement", () => {
|
||||
const error = getValidatedBracketsFromPlacements(
|
||||
"0+",
|
||||
) as Progression.ValidationError;
|
||||
expect(error.type).toBe("PLACEMENTS_PARSE_ERROR");
|
||||
});
|
||||
|
||||
it("rejects rest on negative placement", () => {
|
||||
const error = Progression.validatedBrackets([
|
||||
{
|
||||
id: "1",
|
||||
name: "Bracket 1",
|
||||
type: "double_elimination",
|
||||
settings: {},
|
||||
requiresCheckIn: false,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Bracket 2",
|
||||
type: "single_elimination",
|
||||
settings: {},
|
||||
requiresCheckIn: false,
|
||||
sources: [
|
||||
{
|
||||
bracketId: "1",
|
||||
placements: "-1+",
|
||||
},
|
||||
],
|
||||
},
|
||||
]) as Progression.ValidationError;
|
||||
expect(error.type).toBe("PLACEMENTS_PARSE_ERROR");
|
||||
});
|
||||
|
||||
it("round-trips lone rest via input format", () => {
|
||||
const validated = getValidatedBracketsFromPlacements(
|
||||
"5+",
|
||||
) as Progression.ParsedBracket[];
|
||||
const inputFormat = Progression.validatedBracketsToInputFormat(validated);
|
||||
expect(inputFormat[1].sources?.[0].placements).toBe("5+");
|
||||
});
|
||||
|
||||
it("round-trips combined rest via input format", () => {
|
||||
const validated = getValidatedBracketsFromPlacements(
|
||||
"1,2,3-4,5+",
|
||||
) as Progression.ParsedBracket[];
|
||||
const inputFormat = Progression.validatedBracketsToInputFormat(validated);
|
||||
expect(inputFormat[1].sources?.[0].placements).toBe("1-4,5+");
|
||||
});
|
||||
|
||||
it("destinationByPlacement routes placements beyond the rest threshold", () => {
|
||||
const validated = getValidatedBracketsFromPlacements(
|
||||
"5+",
|
||||
) as Progression.ParsedBracket[];
|
||||
expect(
|
||||
Progression.destinationByPlacement({
|
||||
sourceBracketIdx: 0,
|
||||
placement: 10,
|
||||
progression: validated,
|
||||
}),
|
||||
).toBe(1);
|
||||
expect(
|
||||
Progression.destinationByPlacement({
|
||||
sourceBracketIdx: 0,
|
||||
placement: 4,
|
||||
progression: validated,
|
||||
}),
|
||||
).toBe(null);
|
||||
});
|
||||
|
||||
it("flags SAME_PLACEMENT_TO_MULTIPLE_BRACKETS when two rest sources share a bracket", () => {
|
||||
const error = getValidatedBrackets([
|
||||
{
|
||||
settings: { teamsPerGroup: 8 },
|
||||
type: "round_robin",
|
||||
},
|
||||
{
|
||||
settings: {},
|
||||
type: "single_elimination",
|
||||
sources: [{ bracketId: "0", placements: "1-4" }],
|
||||
},
|
||||
{
|
||||
settings: {},
|
||||
type: "single_elimination",
|
||||
sources: [{ bracketId: "0", placements: "5+" }],
|
||||
},
|
||||
{
|
||||
settings: {},
|
||||
type: "single_elimination",
|
||||
sources: [{ bracketId: "0", placements: "6+" }],
|
||||
},
|
||||
]) as Progression.ValidationError;
|
||||
expect(error.type).toBe("SAME_PLACEMENT_TO_MULTIPLE_BRACKETS");
|
||||
});
|
||||
|
||||
it("flags SAME_PLACEMENT_TO_MULTIPLE_BRACKETS when rest overlaps an explicit placement", () => {
|
||||
const error = getValidatedBrackets([
|
||||
{
|
||||
settings: { teamsPerGroup: 8 },
|
||||
type: "round_robin",
|
||||
},
|
||||
{
|
||||
settings: {},
|
||||
type: "single_elimination",
|
||||
sources: [{ bracketId: "0", placements: "1-4,5+" }],
|
||||
},
|
||||
{
|
||||
settings: {},
|
||||
type: "single_elimination",
|
||||
sources: [{ bracketId: "0", placements: "7" }],
|
||||
},
|
||||
]) as Progression.ValidationError;
|
||||
expect(error.type).toBe("SAME_PLACEMENT_TO_MULTIPLE_BRACKETS");
|
||||
});
|
||||
|
||||
it("still flags TOO_MANY_PLACEMENTS when rest's explicit max exceeds teamsPerGroup", () => {
|
||||
const error = getValidatedBrackets([
|
||||
{
|
||||
settings: { teamsPerGroup: 4 },
|
||||
type: "round_robin",
|
||||
},
|
||||
{
|
||||
settings: {},
|
||||
type: "single_elimination",
|
||||
sources: [{ bracketId: "0", placements: "1-4" }],
|
||||
},
|
||||
{
|
||||
settings: {},
|
||||
type: "single_elimination",
|
||||
sources: [{ bracketId: "0", placements: "5+" }],
|
||||
},
|
||||
]) as Progression.ValidationError;
|
||||
expect(error.type).toBe("TOO_MANY_PLACEMENTS");
|
||||
});
|
||||
});
|
||||
|
||||
const getValidatedBrackets = (
|
||||
brackets: (Omit<
|
||||
Progression.InputBracket,
|
||||
|
||||
@@ -12,6 +12,8 @@ export interface DBSource {
|
||||
bracketIdx: number;
|
||||
/** Team placements that join this bracket. E.g. [1, 2] would mean top 1 & 2 teams. [-1] would mean the last placing teams. Can be empty array for Swiss brackets with early advance. */
|
||||
placements: number[];
|
||||
/** When true, the highest value in `placements` is treated as "and every placement after that". Set by the "N+" rest syntax. Only valid with positive placements. */
|
||||
rest?: boolean;
|
||||
}
|
||||
|
||||
export interface EditableSource {
|
||||
@@ -142,14 +144,14 @@ export function validatedBracketsToInputFormat(
|
||||
bracketId: String(source.bracketIdx),
|
||||
placements:
|
||||
source.placements.length > 0
|
||||
? placementsToString(source.placements)
|
||||
? placementsToString(source.placements, source.rest)
|
||||
: "",
|
||||
})),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function placementsToString(placements: number[]): string {
|
||||
function placementsToString(placements: number[], rest = false): string {
|
||||
if (placements.length === 0) return "";
|
||||
|
||||
placements.sort((a, b) => a - b);
|
||||
@@ -159,28 +161,30 @@ function placementsToString(placements: number[]): string {
|
||||
return placements.join(",");
|
||||
}
|
||||
|
||||
const ranges: string[] = [];
|
||||
let start = placements[0];
|
||||
let end = placements[0];
|
||||
const highest = placements[placements.length - 1];
|
||||
const allButHighest = rest ? placements.slice(0, -1) : placements;
|
||||
|
||||
for (let i = 1; i < placements.length; i++) {
|
||||
if (placements[i] === end + 1) {
|
||||
end = placements[i];
|
||||
} else {
|
||||
if (start === end) {
|
||||
ranges.push(`${start}`);
|
||||
const ranges: string[] = [];
|
||||
|
||||
if (allButHighest.length > 0) {
|
||||
let start = allButHighest[0];
|
||||
let end = allButHighest[0];
|
||||
|
||||
for (let i = 1; i < allButHighest.length; i++) {
|
||||
if (allButHighest[i] === end + 1) {
|
||||
end = allButHighest[i];
|
||||
} else {
|
||||
ranges.push(`${start}-${end}`);
|
||||
ranges.push(start === end ? `${start}` : `${start}-${end}`);
|
||||
start = allButHighest[i];
|
||||
end = allButHighest[i];
|
||||
}
|
||||
start = placements[i];
|
||||
end = placements[i];
|
||||
}
|
||||
|
||||
ranges.push(start === end ? `${start}` : `${start}-${end}`);
|
||||
}
|
||||
|
||||
if (start === end) {
|
||||
ranges.push(String(start));
|
||||
} else {
|
||||
ranges.push(`${start}-${end}`);
|
||||
if (rest) {
|
||||
ranges.push(`${highest}+`);
|
||||
}
|
||||
|
||||
return ranges.join(",");
|
||||
@@ -353,27 +357,28 @@ function toOutputBracketFormat(brackets: InputBracket[]): ParsedBracket[] {
|
||||
? dateToDatabaseTimestamp(bracket.startTime)
|
||||
: undefined,
|
||||
sources: bracket.sources?.map((source) => {
|
||||
const placements = parsePlacements(source.placements);
|
||||
const parsed = parsePlacements(source.placements);
|
||||
const sourceBracketIdx = brackets.findIndex(
|
||||
(b) => b.id === source.bracketId,
|
||||
);
|
||||
const sourceBracket = brackets[sourceBracketIdx];
|
||||
|
||||
// Allow empty placements only for Swiss brackets with early advance
|
||||
if (placements && placements.length === 0) {
|
||||
if (parsed && parsed.placements.length === 0) {
|
||||
const isSwissWithEarlyAdvance =
|
||||
sourceBracket?.type === "swiss" &&
|
||||
sourceBracket?.settings?.advanceThreshold;
|
||||
if (!isSwissWithEarlyAdvance) {
|
||||
throw { badBracketIdx: bracketIdx };
|
||||
}
|
||||
} else if (placements === null) {
|
||||
} else if (parsed === null) {
|
||||
throw { badBracketIdx: bracketIdx };
|
||||
}
|
||||
|
||||
return {
|
||||
bracketIdx: sourceBracketIdx,
|
||||
placements: placements ?? [],
|
||||
placements: parsed?.placements ?? [],
|
||||
...(parsed?.rest ? { rest: true as const } : {}),
|
||||
};
|
||||
}),
|
||||
};
|
||||
@@ -391,18 +396,21 @@ function toOutputBracketFormat(brackets: InputBracket[]): ParsedBracket[] {
|
||||
return result;
|
||||
}
|
||||
|
||||
function parsePlacements(placements: string) {
|
||||
// Handle empty string case
|
||||
function parsePlacements(
|
||||
placements: string,
|
||||
): { placements: number[]; rest: boolean } | null {
|
||||
if (placements.trim() === "") {
|
||||
return [];
|
||||
return { placements: [], rest: false };
|
||||
}
|
||||
|
||||
const parts = placements.split(",");
|
||||
const parts = placements.split(",").map((p) => p.trim());
|
||||
|
||||
const result: number[] = [];
|
||||
let rest = false;
|
||||
|
||||
for (let part of parts) {
|
||||
part = part.trim();
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
const isLast = i === parts.length - 1;
|
||||
|
||||
const isNegative = part.match(/^-\d+$/);
|
||||
if (isNegative) {
|
||||
@@ -410,21 +418,35 @@ function parsePlacements(placements: string) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const restMatch = part.match(/^(\d+)(?:-(\d+))?\+$/);
|
||||
if (restMatch) {
|
||||
if (!isLast || part === "0+") return null;
|
||||
rest = true;
|
||||
|
||||
const start = Number(restMatch[1]);
|
||||
const end = restMatch[2] ? Number(restMatch[2]) : start;
|
||||
if (end < start) return null;
|
||||
for (let n = start; n <= end; n++) {
|
||||
result.push(n);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const isValid = part.match(/^\d+(-\d+)?$/) && part !== "0";
|
||||
if (!isValid) return null;
|
||||
|
||||
if (part.includes("-")) {
|
||||
const [start, end] = part.split("-").map(Number);
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
result.push(i);
|
||||
for (let n = start; n <= end; n++) {
|
||||
result.push(n);
|
||||
}
|
||||
} else {
|
||||
result.push(Number(part));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return { placements: result, rest };
|
||||
}
|
||||
|
||||
function resolvesWinner(brackets: ParsedBracket[]) {
|
||||
@@ -443,6 +465,11 @@ function resolvesWinner(brackets: ParsedBracket[]) {
|
||||
|
||||
function samePlacementToMultipleBrackets(brackets: ParsedBracket[]) {
|
||||
const map = new Map<string, number[]>();
|
||||
// per source bracketIdx: list of { destinationBracketIdx, restFromPlacement }
|
||||
const restSources = new Map<
|
||||
number,
|
||||
{ destinationBracketIdx: number; restFromPlacement: number }[]
|
||||
>();
|
||||
|
||||
for (const [bracketIdx, bracket] of brackets.entries()) {
|
||||
if (!bracket.sources) continue;
|
||||
@@ -457,18 +484,60 @@ function samePlacementToMultipleBrackets(brackets: ParsedBracket[]) {
|
||||
|
||||
map.get(id)!.push(bracketIdx);
|
||||
}
|
||||
|
||||
if (source.rest && source.placements.length > 0) {
|
||||
const positives = source.placements.filter((p) => p > 0);
|
||||
if (positives.length === 0) continue;
|
||||
const restFromPlacement = Math.max(...positives);
|
||||
|
||||
if (!restSources.has(source.bracketIdx)) {
|
||||
restSources.set(source.bracketIdx, []);
|
||||
}
|
||||
restSources.get(source.bracketIdx)!.push({
|
||||
destinationBracketIdx: bracketIdx,
|
||||
restFromPlacement,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result: number[] = [];
|
||||
const result = new Set<number>();
|
||||
|
||||
for (const [_, bracketIdxs] of map) {
|
||||
if (bracketIdxs.length > 1) {
|
||||
result.push(...bracketIdxs);
|
||||
for (const idx of bracketIdxs) result.add(idx);
|
||||
}
|
||||
}
|
||||
|
||||
return result.length ? result : null;
|
||||
for (const [sourceBracketIdx, restList] of restSources) {
|
||||
// multiple "rest" sources from same bracket = conflict
|
||||
if (restList.length > 1) {
|
||||
for (const { destinationBracketIdx } of restList) {
|
||||
result.add(destinationBracketIdx);
|
||||
}
|
||||
}
|
||||
|
||||
// any other source that claims a placement >= restFromPlacement = conflict
|
||||
const restEntry = restList[0];
|
||||
if (!restEntry) continue;
|
||||
for (const [otherBracketIdx, otherBracket] of brackets.entries()) {
|
||||
if (!otherBracket.sources) continue;
|
||||
for (const otherSource of otherBracket.sources) {
|
||||
if (otherSource.bracketIdx !== sourceBracketIdx) continue;
|
||||
if (otherBracketIdx === restEntry.destinationBracketIdx) continue;
|
||||
if (
|
||||
otherSource.placements.some(
|
||||
(p) => p > 0 && p >= restEntry.restFromPlacement,
|
||||
)
|
||||
) {
|
||||
result.add(otherBracketIdx);
|
||||
result.add(restEntry.destinationBracketIdx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.size > 0 ? [...result] : null;
|
||||
}
|
||||
|
||||
function duplicateNames(brackets: ParsedBracket[]) {
|
||||
@@ -973,14 +1042,24 @@ export function destinationByPlacement({
|
||||
);
|
||||
|
||||
const destination = destinations.find((destinationBracketIdx) =>
|
||||
progression[destinationBracketIdx].sources?.some((source) =>
|
||||
source.placements.includes(placement),
|
||||
progression[destinationBracketIdx].sources?.some(
|
||||
(source) =>
|
||||
source.bracketIdx === sourceBracketIdx &&
|
||||
sourceClaimsPlacement(source, placement),
|
||||
),
|
||||
);
|
||||
|
||||
return destination ?? null;
|
||||
}
|
||||
|
||||
function sourceClaimsPlacement(source: DBSource, placement: number): boolean {
|
||||
if (source.placements.includes(placement)) return true;
|
||||
if (source.rest && source.placements.length > 0 && placement > 0) {
|
||||
return placement >= Math.max(...source.placements);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function startingBrackets(progression: ParsedBracket[]): number[] {
|
||||
return progression
|
||||
.map((bracket, idx) => ({ bracket, idx }))
|
||||
|
||||
@@ -229,6 +229,7 @@ export class Tournament {
|
||||
sourceBracket.source({
|
||||
placements: source.placements,
|
||||
advanceThreshold: sourceBracket.settings?.advanceThreshold,
|
||||
rest: source.rest,
|
||||
});
|
||||
if (!relevantMatchesFinished) {
|
||||
allRelevantMatchesFinished = false;
|
||||
|
||||
Reference in New Issue
Block a user