diff --git a/app/features/tournament-bracket/components/Bracket/Bracket.browser.test.tsx b/app/features/tournament-bracket/components/Bracket/Bracket.browser.test.tsx index 1c4e0a710..8c72f8a20 100644 --- a/app/features/tournament-bracket/components/Bracket/Bracket.browser.test.tsx +++ b/app/features/tournament-bracket/components/Bracket/Bracket.browser.test.tsx @@ -117,7 +117,7 @@ function createSingleEliminationData(): TournamentManagerDataSet { number: 1, type: "single_elimination", tournament_id: 1, - settings: { size: 8 }, + settings: {}, }, ], group: [{ id: 1, number: 1, stage_id: 1 }], @@ -228,7 +228,7 @@ function createByeHeavySingleEliminationData(): TournamentManagerDataSet { number: 1, type: "single_elimination", tournament_id: 1, - settings: { size: 8 }, + settings: {}, }, ], group: [{ id: 1, number: 1, stage_id: 1 }], @@ -342,7 +342,7 @@ function createDoubleEliminationData(): TournamentManagerDataSet { number: 1, type: "double_elimination", tournament_id: 1, - settings: { size: 4 }, + settings: {}, }, ], group: [ @@ -443,7 +443,7 @@ function createRoundRobinData(): TournamentManagerDataSet { number: 1, type: "round_robin", tournament_id: 1, - settings: { groupCount: 2, size: 6 }, + settings: { groupCount: 2 }, }, ], group: [ @@ -673,7 +673,7 @@ function createLargeSingleEliminationData(options?: { number: 1, type: "single_elimination", tournament_id: 1, - settings: { size: 16 }, + settings: {}, }, ], group: [{ id: 1, number: 1, stage_id: 1 }], diff --git a/app/features/tournament-bracket/core/engine/create/builder.ts b/app/features/tournament-bracket/core/engine/create/builder.ts index 4770e9415..6280a0ef5 100644 --- a/app/features/tournament-bracket/core/engine/create/builder.ts +++ b/app/features/tournament-bracket/core/engine/create/builder.ts @@ -29,24 +29,22 @@ import { export class StageCreator { readonly input: ResolvedCreateBracketInput; settings: StageSettings; - seeding: Seeding | undefined; + seeding: Seeding; readonly data: BracketData; constructor(input: ResolvedCreateBracketInput) { - this.input = input; - this.settings = structuredClone(input.settings) ?? {}; - const seeding = input.seeding ? [...input.seeding] : undefined; - this.seeding = - seeding && input.type !== "round_robin" - ? padSeedingToPowerOfTwo(seeding) - : seeding; - this.data = { stage: [], group: [], round: [], match: [] }; - if (!input.name) throw Error("You must provide a name for the stage."); if (!Number.isInteger(input.tournamentId)) throw Error("You must provide a tournament id for the stage."); + this.input = input; + this.settings = structuredClone(input.settings) ?? {}; + const seeding = [...input.seeding]; + this.seeding = + input.type !== "round_robin" ? padSeedingToPowerOfTwo(seeding) : seeding; + this.data = { stage: [], group: [], round: [], match: [] }; + if (input.type === "single_elimination") this.settings.consolationFinal = this.settings.consolationFinal || false; } @@ -161,7 +159,7 @@ export class StageCreator { number: number, losers: ParticipantSlot[][], ): ParticipantSlot { - const participantCount = this.settings.size!; + const participantCount = this.seeding.length; const roundPairCount = helpers.getRoundPairCount(participantCount); let losersId = 0; @@ -330,31 +328,13 @@ export class StageCreator { } /** - * Returns a list of slots. - * - If `seeding` was given, uses it. - * - If `size` was given, only returns a list of empty slots. + * Returns the list of slots from the seeding. * * @param positions An optional list of positions (seeds) for a manual ordering. */ getSlots(positions?: number[]): ParticipantSlot[] { - const size = this.settings.size || this.seeding?.length || 0; - helpers.ensureValidSize(this.input.type, size); - - if (size && !this.seeding) - return Array.from(Array(size), (_: ParticipantSlot, i) => ({ - id: null, - position: i + 1, - })); - - if (!this.seeding) throw Error("Either size or seeding must be given."); - - this.settings = { - ...this.settings, - size, // Always set the size. - }; - + helpers.ensureValidSize(this.input.type, this.seeding.length); helpers.ensureNoDuplicates(this.seeding); - this.seeding = helpers.fixSeeding(this.seeding, size); return this.getSlotsUsingIds(this.seeding, positions); } diff --git a/app/features/tournament-bracket/core/engine/create/double-elimination.test.ts b/app/features/tournament-bracket/core/engine/create/double-elimination.test.ts index dd61070b6..ddaf1833e 100644 --- a/app/features/tournament-bracket/core/engine/create/double-elimination.test.ts +++ b/app/features/tournament-bracket/core/engine/create/double-elimination.test.ts @@ -31,7 +31,7 @@ describe("Create double elimination stage", () => { name: "Example with 256 participants", tournamentId: 0, type: "double_elimination", - settings: { size: 256 }, + seeding: Array.from({ length: 256 }, (_, i) => i + 1), }); }); diff --git a/app/features/tournament-bracket/core/engine/create/double-elimination.ts b/app/features/tournament-bracket/core/engine/create/double-elimination.ts index 531917320..37d44d9bd 100644 --- a/app/features/tournament-bracket/core/engine/create/double-elimination.ts +++ b/app/features/tournament-bracket/core/engine/create/double-elimination.ts @@ -21,7 +21,7 @@ export function createDoubleElimination(creator: StageCreator): void { ordered, ); - if (helpers.isDoubleEliminationNecessary(creator.settings.size!)) { + if (helpers.isDoubleEliminationNecessary(slots.length)) { const winnerLb = creator.createLowerBracket(stage.id, 2, losersWb); createGrandFinal(creator, stage.id, winnerWb, winnerLb); } diff --git a/app/features/tournament-bracket/core/engine/create/index.ts b/app/features/tournament-bracket/core/engine/create/index.ts index 24a533131..a929b4d00 100644 --- a/app/features/tournament-bracket/core/engine/create/index.ts +++ b/app/features/tournament-bracket/core/engine/create/index.ts @@ -31,7 +31,7 @@ export function create(input: CreateBracketInput): CreatedBracket { /** * Engine-internal `create` taking already-resolved internal stage settings. * Tests use this to control knobs that are an implementation detail to the - * app (seed ordering, byes balancing, TBD slots via `settings.size`). + * app (seed ordering, byes balancing). */ export function createResolved( input: ResolvedCreateBracketInput, diff --git a/app/features/tournament-bracket/core/engine/create/round-robin.test.ts b/app/features/tournament-bracket/core/engine/create/round-robin.test.ts index ae9edba2d..1ac9b11ad 100644 --- a/app/features/tournament-bracket/core/engine/create/round-robin.test.ts +++ b/app/features/tournament-bracket/core/engine/create/round-robin.test.ts @@ -144,14 +144,14 @@ describe("Create a round-robin stage", () => { expect(realMatchRound.number).toBe(1); }); - test("should create a round-robin stage with to be determined participants", () => { + test("should create a round-robin stage split across multiple groups", () => { bracket.create({ name: "Example", tournamentId: 0, type: "round_robin", + seeding: Array.from({ length: 16 }, (_, i) => i + 1), settings: { groupCount: 4, - size: 16, }, }); @@ -181,6 +181,7 @@ describe("Create a round-robin stage", () => { name: "Example", tournamentId: 0, type: "round_robin", + seeding: [1, 2, 3, 4], }), ).toThrow("You must specify a group count for round-robin stages."); }); @@ -191,9 +192,9 @@ describe("Create a round-robin stage", () => { name: "Example", tournamentId: 0, type: "round_robin", + seeding: [1, 2, 3, 4], settings: { groupCount: 0, - size: 4, }, }), ).toThrow("You must provide a strictly positive group count."); diff --git a/app/features/tournament-bracket/core/engine/general.test.ts b/app/features/tournament-bracket/core/engine/general.test.ts index 0dc24a812..a3598227a 100644 --- a/app/features/tournament-bracket/core/engine/general.test.ts +++ b/app/features/tournament-bracket/core/engine/general.test.ts @@ -37,10 +37,8 @@ describe("BYE handling", () => { name: "Example with BYEs", tournamentId: 0, type: "double_elimination", - seeding: [1, 2], - settings: { - size: 4, - }, + seeding: [1, 2, null, null], + settings: {}, }); expect(bracket.match(0).opponent1?.id).toBe(1); @@ -59,9 +57,8 @@ describe("Position checks", () => { name: "Example with double grand final", tournamentId: 0, type: "double_elimination", - settings: { - size: 8, - }, + seeding: [1, 2, 3, 4, 5, 6, 7, 8], + settings: {}, }); }); @@ -132,37 +129,22 @@ describe("Special cases", () => { expect(bracket.match(0).opponent2).toBe(null); }); - test("should throw if the size of a stage is not a power of two", () => { - expect(() => - bracket.create({ - name: "Example", - tournamentId: 0, - type: "single_elimination", - settings: { size: 3 }, - }), - ).toThrow( - "The library only supports a participant count which is a power of two.", - ); - }); - test("should throw if the participant count of a stage is less than two", () => { expect(() => bracket.create({ name: "Example", tournamentId: 0, type: "single_elimination", - settings: { size: 0 }, + seeding: [], }), - ).toThrow( - "Impossible to create an empty stage. If you want an empty seeding, just set the size of the stage.", - ); + ).toThrow("Impossible to create a stage with less than 2 participants."); expect(() => bracket.create({ name: "Example", tournamentId: 0, type: "single_elimination", - settings: { size: 1 }, + seeding: [1], }), ).toThrow("Impossible to create a stage with less than 2 participants."); }); @@ -218,9 +200,7 @@ describe("Reset match", () => { tournamentId: 0, type: "single_elimination", seeding: [1, null, null, null, null, null, null, 2], - settings: { - size: 8, - }, + settings: {}, }); bracket.updateMatch({ diff --git a/app/features/tournament-bracket/core/engine/helpers.ts b/app/features/tournament-bracket/core/engine/helpers.ts index 985f736f6..efc68a321 100644 --- a/app/features/tournament-bracket/core/engine/helpers.ts +++ b/app/features/tournament-bracket/core/engine/helpers.ts @@ -8,7 +8,6 @@ import type { ParticipantResult, ParticipantSlot, Result, - Seeding, SeedOrdering, Side, StageData, @@ -294,27 +293,6 @@ export function ensureNoDuplicates(array: (T | null)[]): void { throw new Error("The seeding has a duplicate participant."); } -/** - * Fixes the seeding by enlarging it if it's not complete. - * - * @param seeding The seeding of the stage. - * @param participantCount The number of participants in the stage. - */ -export function fixSeeding( - seeding: Seeding, - participantCount: number, -): Seeding { - if (seeding.length > participantCount) - throw Error( - "The seeding has more participants than the size of the stage.", - ); - - if (seeding.length < participantCount) - return Array.from(Array(participantCount), (_, i) => seeding[i] || null); - - return seeding; -} - /** * Ensures that the participant count is valid. * @@ -325,11 +303,6 @@ export function ensureValidSize( stageType: StageType, participantCount: number, ): void { - if (participantCount === 0) - throw Error( - "Impossible to create an empty stage. If you want an empty seeding, just set the size of the stage.", - ); - if (participantCount < 2) throw Error("Impossible to create a stage with less than 2 participants."); diff --git a/app/features/tournament-bracket/core/engine/propagation/traversal.ts b/app/features/tournament-bracket/core/engine/propagation/traversal.ts index 50fbc0127..f51a73c55 100644 --- a/app/features/tournament-bracket/core/engine/propagation/traversal.ts +++ b/app/features/tournament-bracket/core/engine/propagation/traversal.ts @@ -417,7 +417,7 @@ export class Propagator { roundNumber: number, ): MatchData[] { if (stage.type === "single_elimination") - return this.getPreviousMatchesFinalSingleElimination(match, stage); + return this.getPreviousMatchesFinalSingleElimination(match); return this.getPreviousMatchesFinalDoubleElimination(match, roundNumber); } @@ -426,15 +426,13 @@ export class Propagator { * Gets the matches leading to the given match, which is in a final group (consolation final). * * @param match The current match. - * @param stage The parent stage. */ private getPreviousMatchesFinalSingleElimination( match: MatchData, - stage: StageData, ): MatchData[] { const upperBracket = this.getUpperBracket(match.stage_id); const upperBracketRoundCount = helpers.getUpperBracketRoundCount( - stage.settings.size!, + this.participantCount(match.stage_id), ); const semiFinalsRound = this.store.selectFirst("round", { @@ -648,7 +646,7 @@ export class Propagator { const roundNumberLB = roundNumber > 1 ? (roundNumber - 1) * 2 : 1; - const participantCount = stage.settings.size!; + const participantCount = this.participantCount(match.stage_id); const method = helpers.getLoserOrdering(participantCount, roundNumberLB); const actualMatchNumberLB = helpers.findLoserMatchNumber( participantCount, @@ -860,6 +858,26 @@ export class Propagator { return winnerBracket; } + /** + * Gets the participant count of an elimination stage, derived from its upper + * bracket's first round (two participants per match, BYEs included). + * + * @param stageId ID of the stage. + */ + private participantCount(stageId: number): number { + const upperBracket = this.getUpperBracket(stageId); + const firstRound = this.store.selectFirst("round", { + group_id: upperBracket.id, + number: 1, + }); + if (!firstRound) throw Error("First round not found."); + + const firstRoundMatches = this.store.selectAll("match", { + round_id: firstRound.id, + }); + return firstRoundMatches.length * 2; + } + /** * Gets the loser bracket. * diff --git a/app/features/tournament-bracket/core/engine/types.ts b/app/features/tournament-bracket/core/engine/types.ts index 4162c17d1..94d833edd 100644 --- a/app/features/tournament-bracket/core/engine/types.ts +++ b/app/features/tournament-bracket/core/engine/types.ts @@ -58,10 +58,6 @@ export type MatchStatus = (typeof MatchStatus)[keyof typeof MatchStatus]; * TournamentStage.settings today (the old brackets-model StageSettings). */ export interface StageSettings { - // xxx: why not inferred? - /** The number of participants. */ - size?: number; - /** Number of groups in a round-robin stage. */ groupCount?: number; @@ -230,15 +226,10 @@ export interface CreateBracketInput { /** * Engine-internal variant of {@link CreateBracketInput}: settings are the - * already-resolved internal {@link StageSettings} and seeding may be omitted - * in favor of `settings.size` (TBD slots). + * already-resolved internal {@link StageSettings}. */ export interface ResolvedCreateBracketInput - extends Omit< - CreateBracketInput, - "seeding" | "settings" | "independentRounds" - > { - seeding?: Seeding; + extends Omit { settings: StageSettings; } diff --git a/app/features/tournament-bracket/core/tests/mocks-li.ts b/app/features/tournament-bracket/core/tests/mocks-li.ts index 329606fd4..0f9b4d0b8 100644 --- a/app/features/tournament-bracket/core/tests/mocks-li.ts +++ b/app/features/tournament-bracket/core/tests/mocks-li.ts @@ -22,9 +22,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ id: 1429, name: "Beta Bracket", number: 2, - settings: { - size: 16, - }, + settings: {}, tournament_id: 815, type: "double_elimination", createdAt: 1734286247, @@ -33,9 +31,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ id: 1430, name: "Alpha Bracket", number: 3, - settings: { - size: 16, - }, + settings: {}, tournament_id: 815, type: "double_elimination", createdAt: 1734286249, @@ -44,9 +40,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({ id: 1431, name: "Gamma Bracket", number: 4, - settings: { - size: 16, - }, + settings: {}, tournament_id: 815, type: "double_elimination", createdAt: 1734286340, diff --git a/app/features/tournament-bracket/core/tests/mocks-sos.ts b/app/features/tournament-bracket/core/tests/mocks-sos.ts index b96028151..aa0afc026 100644 --- a/app/features/tournament-bracket/core/tests/mocks-sos.ts +++ b/app/features/tournament-bracket/core/tests/mocks-sos.ts @@ -12,7 +12,6 @@ export const SWIM_OR_SINK_167 = ( number: 1, settings: { groupCount: 11, - size: 44, }, tournament_id: 672, type: "round_robin", diff --git a/app/features/tournament-bracket/core/tests/mocks.ts b/app/features/tournament-bracket/core/tests/mocks.ts index 25d57b837..6eb9cd861 100644 --- a/app/features/tournament-bracket/core/tests/mocks.ts +++ b/app/features/tournament-bracket/core/tests/mocks.ts @@ -10,7 +10,6 @@ export const PADDLING_POOL_257 = () => number: 1, settings: { groupCount: 9, - size: 35, }, tournament_id: 27, type: "round_robin", @@ -6822,7 +6821,6 @@ export const PADDLING_POOL_255 = () => number: 1, settings: { groupCount: 9, - size: 35, }, tournament_id: 18, type: "round_robin", @@ -14210,9 +14208,7 @@ export const IN_THE_ZONE_32 = ({ id: 16, name: "Main bracket", number: 1, - settings: { - size: 32, - }, + settings: {}, tournament_id: 11, type: "double_elimination", },