Remove size

This commit is contained in:
Kalle 2026-07-23 18:48:51 +03:00
parent 6d96069d56
commit 5facba4e42
13 changed files with 60 additions and 128 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.");

View File

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

View File

@ -8,7 +8,6 @@ import type {
ParticipantResult,
ParticipantSlot,
Result,
Seeding,
SeedOrdering,
Side,
StageData,
@ -294,27 +293,6 @@ export function ensureNoDuplicates<T>(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.");

View File

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

View File

@ -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<CreateBracketInput, "settings" | "independentRounds"> {
settings: StageSettings;
}

View File

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

View File

@ -12,7 +12,6 @@ export const SWIM_OR_SINK_167 = (
number: 1,
settings: {
groupCount: 11,
size: 44,
},
tournament_id: 672,
type: "round_robin",

View File

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