Label underground bracket in the FactCard

This commit is contained in:
Kalle
2026-06-12 17:26:36 +03:00
parent 2398c4e1d2
commit facb0e358e
2 changed files with 82 additions and 20 deletions

View File

@@ -797,17 +797,26 @@ describe("tournamentNameParts", () => {
});
describe("bracketProgressionLabel", () => {
const bracket = (
bracket: Partial<ParsedBracket> & Pick<ParsedBracket, "type">,
): ParsedBracket => ({
name: bracket.type,
requiresCheckIn: false,
settings: {},
...bracket,
});
it("returns the short code for a single stage", () => {
expect(bracketProgressionLabel([{ type: "single_elimination" }])).toBe(
"SE",
);
expect(
bracketProgressionLabel([bracket({ type: "single_elimination" })]),
).toBe("SE");
});
it("joins stages with an arrow", () => {
expect(
bracketProgressionLabel([
{ type: "round_robin" },
{ type: "single_elimination" },
bracket({ type: "round_robin" }),
bracket({ type: "single_elimination" }),
]),
).toBe("RR → SE");
});
@@ -815,13 +824,45 @@ describe("bracketProgressionLabel", () => {
it("collapses consecutive duplicate stages", () => {
expect(
bracketProgressionLabel([
{ type: "single_elimination" },
{ type: "single_elimination" },
{ type: "double_elimination" },
bracket({ type: "single_elimination" }),
bracket({ type: "single_elimination" }),
bracket({ type: "double_elimination" }),
]),
).toBe("SE → DE");
});
it("appends an underground bracket of a new type with a plus and (UG) tag", () => {
expect(
bracketProgressionLabel([
bracket({ type: "double_elimination" }),
bracket({
type: "single_elimination",
sources: [{ bracketIdx: 0, placements: [-1] }],
}),
]),
).toBe("DE + SE (UG)");
});
it("collapses repeated underground brackets of an already-shown type", () => {
expect(
bracketProgressionLabel([
bracket({ type: "round_robin" }),
bracket({
type: "single_elimination",
sources: [{ bracketIdx: 0, placements: [1] }],
}),
bracket({
type: "single_elimination",
sources: [{ bracketIdx: 0, placements: [2] }],
}),
bracket({
type: "single_elimination",
sources: [{ bracketIdx: 0, placements: [3] }],
}),
]),
).toBe("RR → SE (UG)");
});
it("returns empty string for empty progression", () => {
expect(bracketProgressionLabel([])).toBe("");
});

View File

@@ -500,30 +500,51 @@ const STAGE_TYPE_TO_SHORT_CODE: Record<
};
/**
* Builds a compact arrow-separated label describing the bracket progression of a tournament,
* Builds a compact label describing the bracket progression of a tournament,
* derived from `settings.bracketProgression`.
*
* Each stage type is rendered as a short code (`RR`, `SE`, `DE`, `SW`) and consecutive duplicates
* are collapsed so e.g. two single-elimination stages still render as a single `SE`.
* Each stage type is rendered as a short code (`RR`, `SE`, `DE`, `SW`). Main progression stages are
* arrow-separated with consecutive duplicates collapsed (e.g. two single-elimination stages still
* render as a single `SE`).
*
* Underground brackets are not part of the main progression — they give early losers another chance
* to play. When present the whole label is tagged `(UG)` once. An underground bracket type that does
* not already appear in the main progression is also surfaced with `+ {code}`, while repeated
* underground brackets of an already-shown type are collapsed away to keep the label compact.
*
* @example
* // [{type: "round_robin"}, {type: "single_elimination"}]
* bracketProgressionLabel(progression) // "RR → SE"
* // [{type: "round_robin"}, {type: "single_elimination"}, ...underground SE brackets]
* bracketProgressionLabel(progression) // "RR → SE (UG)"
* // [{type: "double_elimination"}, {type: "single_elimination", sources: [...]}]
* bracketProgressionLabel(progression) // "DE + SE (UG)"
*/
export function bracketProgressionLabel(
progression: Pick<ParsedBracket, "type">[],
): string {
export function bracketProgressionLabel(progression: ParsedBracket[]): string {
if (progression.length === 0) return "";
const codes: string[] = [];
for (const bracket of progression) {
const code = STAGE_TYPE_TO_SHORT_CODE[bracket.type];
const mainCodes: string[] = [];
const undergroundCodes: string[] = [];
for (let i = 0; i < progression.length; i++) {
const code = STAGE_TYPE_TO_SHORT_CODE[progression[i].type];
const codes = Progression.isUnderground(i, progression)
? undergroundCodes
: mainCodes;
if (codes.at(-1) !== code) {
codes.push(code);
}
}
return codes.join(" → ");
const mainLabel = mainCodes.join(" → ");
if (undergroundCodes.length === 0) return mainLabel;
const extraCodes = undergroundCodes.filter(
(code) => !mainCodes.includes(code),
);
const label =
extraCodes.length > 0 ? [mainLabel, ...extraCodes].join(" + ") : mainLabel;
return `${label} (UG)`;
}
/**