Make seed padding implementation detail

This commit is contained in:
Kalle 2026-07-22 21:20:49 +03:00
parent d4d01d9bc3
commit 3e87dc0adc
9 changed files with 39 additions and 69 deletions

View File

@ -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,
});
}

View File

@ -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,
});

View File

@ -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
: {

View File

@ -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,

View File

@ -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: [] };

View File

@ -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.
*

View File

@ -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",

View File

@ -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" },

View File

@ -5,13 +5,6 @@ import type { Standing } from "./core/Bracket";
export const tournamentWebsocketRoom = (tournamentId: number) =>
`tournament__${tournamentId}`;
export function fillWithNullTillPowerOfTwo<T>(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.
*