From 3e87dc0adc676d87891c50fe411e67b76aa2f1a7 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Wed, 22 Jul 2026 21:20:49 +0300 Subject: [PATCH] Make seed padding implementation detail --- .../bracket-test/routes/bracket-test.tsx | 6 +---- .../actions/to.$id.brackets.server.ts | 11 ++------ .../core/Bracket/Bracket.ts | 6 +---- .../tournament-bracket/core/Tournament.ts | 7 ++---- .../core/engine/create/builder.ts | 13 ++++++++-- .../core/engine/create/seeding.ts | 9 +++++++ .../core/engine/general.test.ts | 24 ++++++++++-------- .../tournament-bracket-utils.test.ts | 25 ------------------- .../tournament-bracket-utils.ts | 7 ------ 9 files changed, 39 insertions(+), 69 deletions(-) diff --git a/app/features/bracket-test/routes/bracket-test.tsx b/app/features/bracket-test/routes/bracket-test.tsx index a53fd0573..200181aaa 100644 --- a/app/features/bracket-test/routes/bracket-test.tsx +++ b/app/features/bracket-test/routes/bracket-test.tsx @@ -9,7 +9,6 @@ import type { Tables } from "~/db/tables"; import type { Bracket as BracketType } from "~/features/tournament-bracket/core/Bracket"; import * as Engine from "~/features/tournament-bracket/core/engine"; import type { TournamentManagerDataSet } from "~/features/tournament-bracket/core/engine/types"; -import { fillWithNullTillPowerOfTwo } from "~/features/tournament-bracket/tournament-bracket-utils"; import styles from "../bracket-test.module.css"; type FormatType = Tables["TournamentStage"]["type"]; @@ -292,9 +291,6 @@ function generateBracketData( }); } - const seeding = - format === "round_robin" ? teamIds : fillWithNullTillPowerOfTwo(teamIds); - const settings = format === "single_elimination" ? { consolationFinal: false } @@ -309,7 +305,7 @@ function generateBracketData( tournamentId: 1, name: "Test Bracket", type: format, - seeding, + seeding: teamIds, settings, }); } diff --git a/app/features/tournament-bracket/actions/to.$id.brackets.server.ts b/app/features/tournament-bracket/actions/to.$id.brackets.server.ts index b7ca76170..58b08cc45 100644 --- a/app/features/tournament-bracket/actions/to.$id.brackets.server.ts +++ b/app/features/tournament-bracket/actions/to.$id.brackets.server.ts @@ -33,10 +33,7 @@ import { tournamentFromDB, } from "../core/Tournament.server"; import { bracketSchema } from "../tournament-bracket-schemas.server"; -import { - fillWithNullTillPowerOfTwo, - tournamentWebsocketRoom, -} from "../tournament-bracket-utils"; +import { tournamentWebsocketRoom } from "../tournament-bracket-utils"; export const action: ActionFunction = async ({ params, request }) => { const user = requireUser(); @@ -109,11 +106,7 @@ export const action: ActionFunction = async ({ params, request }) => { // xxx: will we really need name? name: bracket.name, type: bracket.type, - // xxx: this could be implementation detail - seeding: - bracket.type === "round_robin" || bracket.type === "swiss" - ? seeding - : fillWithNullTillPowerOfTwo(seeding), + seeding, settings, abDivisions, }); diff --git a/app/features/tournament-bracket/core/Bracket/Bracket.ts b/app/features/tournament-bracket/core/Bracket/Bracket.ts index 8e51e7ac9..4835daeb9 100644 --- a/app/features/tournament-bracket/core/Bracket/Bracket.ts +++ b/app/features/tournament-bracket/core/Bracket/Bracket.ts @@ -8,7 +8,6 @@ import type { } from "~/features/tournament-bracket/core/engine/types"; import invariant from "~/utils/invariant"; import { logger } from "~/utils/logger"; -import { fillWithNullTillPowerOfTwo } from "../../tournament-bracket-utils"; import * as AbDivisions from "../AbDivisions"; import * as Engine from "../engine"; import * as Progression from "../Progression"; @@ -308,10 +307,7 @@ export abstract class Bracket { tournamentId: virtualTournamentId, name: "Virtual", type: this.type, - seeding: - this.type === "round_robin" - ? teams - : fillWithNullTillPowerOfTwo(teams), + seeding: teams, settings: abDivisions ? settings : { diff --git a/app/features/tournament-bracket/core/Tournament.ts b/app/features/tournament-bracket/core/Tournament.ts index cbd84713c..6bec7098b 100644 --- a/app/features/tournament-bracket/core/Tournament.ts +++ b/app/features/tournament-bracket/core/Tournament.ts @@ -29,10 +29,7 @@ import { import invariant from "~/utils/invariant"; import { logger } from "~/utils/logger"; import { assertUnreachable } from "~/utils/types"; -import { - fillWithNullTillPowerOfTwo, - groupNumberToLetters, -} from "../tournament-bracket-utils"; +import { groupNumberToLetters } from "../tournament-bracket-utils"; import { type Bracket, createBracket } from "./Bracket"; import * as Engine from "./engine"; import { getRounds } from "./rounds"; @@ -396,7 +393,7 @@ export class Tournament { TournamentStage["type"], "round_robin" | "swiss" >, - seeding: fillWithNullTillPowerOfTwo(candidateTeams), + seeding: candidateTeams, settings: this.bracketManagerSettings( settings, bracket.type, diff --git a/app/features/tournament-bracket/core/engine/create/builder.ts b/app/features/tournament-bracket/core/engine/create/builder.ts index d5d086a4e..884b0e495 100644 --- a/app/features/tournament-bracket/core/engine/create/builder.ts +++ b/app/features/tournament-bracket/core/engine/create/builder.ts @@ -14,7 +14,12 @@ import type { StandardBracketResults, } from "../types"; import { MatchStatus } from "../types"; -import { balanceByes, defaultMinorOrdering, ordering } from "./seeding"; +import { + balanceByes, + defaultMinorOrdering, + ordering, + padSeedingToPowerOfTwo, +} from "./seeding"; /** * Accumulates the rows of a stage being created. Pure port of the old @@ -32,7 +37,11 @@ export class StageCreator { constructor(input: CreateBracketInput) { this.input = input; this.settings = structuredClone(input.settings) ?? {}; - this.seeding = input.seeding ? [...input.seeding] : undefined; + const seeding = input.seeding ? [...input.seeding] : undefined; + this.seeding = + seeding && input.type !== "round_robin" + ? padSeedingToPowerOfTwo(seeding) + : seeding; this.seedOrdering = this.settings.seedOrdering || []; this.data = { stage: [], group: [], round: [], match: [] }; diff --git a/app/features/tournament-bracket/core/engine/create/seeding.ts b/app/features/tournament-bracket/core/engine/create/seeding.ts index cdc92a478..fe7098a97 100644 --- a/app/features/tournament-bracket/core/engine/create/seeding.ts +++ b/app/features/tournament-bracket/core/engine/create/seeding.ts @@ -175,6 +175,15 @@ export function balanceByes( return setArraySize(flat, size, null); } +/** + * Pads the seeding with BYEs (`null`) until its length is a power of two. + * + * @param seeding The seeding of the stage. + */ +export function padSeedingToPowerOfTwo(seeding: Seeding): Seeding { + return setArraySize(seeding, getNearestPowerOfTwo(seeding.length), null); +} + /** * Sets the size of an array with a placeholder if the size is bigger. * diff --git a/app/features/tournament-bracket/core/engine/general.test.ts b/app/features/tournament-bracket/core/engine/general.test.ts index 40cea9648..1a40d9908 100644 --- a/app/features/tournament-bracket/core/engine/general.test.ts +++ b/app/features/tournament-bracket/core/engine/general.test.ts @@ -141,18 +141,20 @@ describe("Special cases", () => { ).toThrow("You must provide a tournament id for the stage."); }); - test("should throw if the participant count of a stage is not a power of two", () => { - expect(() => - bracket.create({ - name: "Example", - tournamentId: 0, - type: "single_elimination", - seeding: [1, 2, 3, 4, 5, 6, 7], - }), - ).toThrow( - "The library only supports a participant count which is a power of two.", - ); + test("should pad the seeding with BYEs to the next power of two", () => { + bracket.create({ + name: "Example", + tournamentId: 0, + type: "single_elimination", + seeding: [1, 2, 3, 4, 5, 6, 7], + settings: { seedOrdering: ["natural"] }, + }); + expect(bracket.match(3).opponent1?.id).toBe(7); + expect(bracket.match(3).opponent2).toBe(null); + }); + + test("should throw if the size of a stage is not a power of two", () => { expect(() => bracket.create({ name: "Example", diff --git a/app/features/tournament-bracket/tournament-bracket-utils.test.ts b/app/features/tournament-bracket/tournament-bracket-utils.test.ts index 617ebc08a..7ab5d5665 100644 --- a/app/features/tournament-bracket/tournament-bracket-utils.test.ts +++ b/app/features/tournament-bracket/tournament-bracket-utils.test.ts @@ -1,34 +1,9 @@ import { describe, expect, test } from "vitest"; import { - fillWithNullTillPowerOfTwo, groupNumberToLetters, validateBadgeReceivers, } from "./tournament-bracket-utils"; -const powerOfTwoParamsToResults: [ - amountOfTeams: number, - expectedNullCount: number, -][] = [ - [32, 0], - [16, 0], - [8, 0], - [31, 1], - [0, 0], - [17, 15], -]; - -describe("fillWithNullTillPowerOfTwo()", () => { - for (const [amountOfTeams, expectedNullCount] of powerOfTwoParamsToResults) { - test(`amountOfTeams=${amountOfTeams} -> ${expectedNullCount}`, () => { - expect( - fillWithNullTillPowerOfTwo(Array(amountOfTeams).fill("team")).filter( - (x) => x === null, - ).length, - ).toBe(expectedNullCount); - }); - } -}); - const groupNumberToLettersParamsToResult = [ { groupNumber: 1, expected: "A" }, { groupNumber: 26, expected: "Z" }, diff --git a/app/features/tournament-bracket/tournament-bracket-utils.ts b/app/features/tournament-bracket/tournament-bracket-utils.ts index f45683015..77b009818 100644 --- a/app/features/tournament-bracket/tournament-bracket-utils.ts +++ b/app/features/tournament-bracket/tournament-bracket-utils.ts @@ -5,13 +5,6 @@ import type { Standing } from "./core/Bracket"; export const tournamentWebsocketRoom = (tournamentId: number) => `tournament__${tournamentId}`; -export function fillWithNullTillPowerOfTwo(arr: T[]) { - const nextPowerOfTwo = 2 ** Math.ceil(Math.log2(arr.length)); - const nullsToAdd = nextPowerOfTwo - arr.length; - - return [...arr, ...new Array(nullsToAdd).fill(null)]; -} - /** * Converts a group number to its corresponding letter representation. *