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, number: 1,
type: "single_elimination", type: "single_elimination",
tournament_id: 1, tournament_id: 1,
settings: { size: 8 }, settings: {},
}, },
], ],
group: [{ id: 1, number: 1, stage_id: 1 }], group: [{ id: 1, number: 1, stage_id: 1 }],
@@ -228,7 +228,7 @@ function createByeHeavySingleEliminationData(): TournamentManagerDataSet {
number: 1, number: 1,
type: "single_elimination", type: "single_elimination",
tournament_id: 1, tournament_id: 1,
settings: { size: 8 }, settings: {},
}, },
], ],
group: [{ id: 1, number: 1, stage_id: 1 }], group: [{ id: 1, number: 1, stage_id: 1 }],
@@ -342,7 +342,7 @@ function createDoubleEliminationData(): TournamentManagerDataSet {
number: 1, number: 1,
type: "double_elimination", type: "double_elimination",
tournament_id: 1, tournament_id: 1,
settings: { size: 4 }, settings: {},
}, },
], ],
group: [ group: [
@@ -443,7 +443,7 @@ function createRoundRobinData(): TournamentManagerDataSet {
number: 1, number: 1,
type: "round_robin", type: "round_robin",
tournament_id: 1, tournament_id: 1,
settings: { groupCount: 2, size: 6 }, settings: { groupCount: 2 },
}, },
], ],
group: [ group: [
@@ -673,7 +673,7 @@ function createLargeSingleEliminationData(options?: {
number: 1, number: 1,
type: "single_elimination", type: "single_elimination",
tournament_id: 1, tournament_id: 1,
settings: { size: 16 }, settings: {},
}, },
], ],
group: [{ id: 1, number: 1, stage_id: 1 }], group: [{ id: 1, number: 1, stage_id: 1 }],

View File

@@ -29,24 +29,22 @@ import {
export class StageCreator { export class StageCreator {
readonly input: ResolvedCreateBracketInput; readonly input: ResolvedCreateBracketInput;
settings: StageSettings; settings: StageSettings;
seeding: Seeding | undefined; seeding: Seeding;
readonly data: BracketData; readonly data: BracketData;
constructor(input: ResolvedCreateBracketInput) { 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 (!input.name) throw Error("You must provide a name for the stage.");
if (!Number.isInteger(input.tournamentId)) if (!Number.isInteger(input.tournamentId))
throw Error("You must provide a tournament id for the stage."); 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") if (input.type === "single_elimination")
this.settings.consolationFinal = this.settings.consolationFinal || false; this.settings.consolationFinal = this.settings.consolationFinal || false;
} }
@@ -161,7 +159,7 @@ export class StageCreator {
number: number, number: number,
losers: ParticipantSlot[][], losers: ParticipantSlot[][],
): ParticipantSlot { ): ParticipantSlot {
const participantCount = this.settings.size!; const participantCount = this.seeding.length;
const roundPairCount = helpers.getRoundPairCount(participantCount); const roundPairCount = helpers.getRoundPairCount(participantCount);
let losersId = 0; let losersId = 0;
@@ -330,31 +328,13 @@ export class StageCreator {
} }
/** /**
* Returns a list of slots. * Returns the list of slots from the seeding.
* - If `seeding` was given, uses it.
* - If `size` was given, only returns a list of empty slots.
* *
* @param positions An optional list of positions (seeds) for a manual ordering. * @param positions An optional list of positions (seeds) for a manual ordering.
*/ */
getSlots(positions?: number[]): ParticipantSlot[] { getSlots(positions?: number[]): ParticipantSlot[] {
const size = this.settings.size || this.seeding?.length || 0; helpers.ensureValidSize(this.input.type, this.seeding.length);
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.ensureNoDuplicates(this.seeding); helpers.ensureNoDuplicates(this.seeding);
this.seeding = helpers.fixSeeding(this.seeding, size);
return this.getSlotsUsingIds(this.seeding, positions); return this.getSlotsUsingIds(this.seeding, positions);
} }

View File

@@ -31,7 +31,7 @@ describe("Create double elimination stage", () => {
name: "Example with 256 participants", name: "Example with 256 participants",
tournamentId: 0, tournamentId: 0,
type: "double_elimination", 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, ordered,
); );
if (helpers.isDoubleEliminationNecessary(creator.settings.size!)) { if (helpers.isDoubleEliminationNecessary(slots.length)) {
const winnerLb = creator.createLowerBracket(stage.id, 2, losersWb); const winnerLb = creator.createLowerBracket(stage.id, 2, losersWb);
createGrandFinal(creator, stage.id, winnerWb, winnerLb); 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. * Engine-internal `create` taking already-resolved internal stage settings.
* Tests use this to control knobs that are an implementation detail to the * 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( export function createResolved(
input: ResolvedCreateBracketInput, input: ResolvedCreateBracketInput,

View File

@@ -144,14 +144,14 @@ describe("Create a round-robin stage", () => {
expect(realMatchRound.number).toBe(1); 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({ bracket.create({
name: "Example", name: "Example",
tournamentId: 0, tournamentId: 0,
type: "round_robin", type: "round_robin",
seeding: Array.from({ length: 16 }, (_, i) => i + 1),
settings: { settings: {
groupCount: 4, groupCount: 4,
size: 16,
}, },
}); });
@@ -181,6 +181,7 @@ describe("Create a round-robin stage", () => {
name: "Example", name: "Example",
tournamentId: 0, tournamentId: 0,
type: "round_robin", type: "round_robin",
seeding: [1, 2, 3, 4],
}), }),
).toThrow("You must specify a group count for round-robin stages."); ).toThrow("You must specify a group count for round-robin stages.");
}); });
@@ -191,9 +192,9 @@ describe("Create a round-robin stage", () => {
name: "Example", name: "Example",
tournamentId: 0, tournamentId: 0,
type: "round_robin", type: "round_robin",
seeding: [1, 2, 3, 4],
settings: { settings: {
groupCount: 0, groupCount: 0,
size: 4,
}, },
}), }),
).toThrow("You must provide a strictly positive group count."); ).toThrow("You must provide a strictly positive group count.");

View File

@@ -37,10 +37,8 @@ describe("BYE handling", () => {
name: "Example with BYEs", name: "Example with BYEs",
tournamentId: 0, tournamentId: 0,
type: "double_elimination", type: "double_elimination",
seeding: [1, 2], seeding: [1, 2, null, null],
settings: { settings: {},
size: 4,
},
}); });
expect(bracket.match(0).opponent1?.id).toBe(1); expect(bracket.match(0).opponent1?.id).toBe(1);
@@ -59,9 +57,8 @@ describe("Position checks", () => {
name: "Example with double grand final", name: "Example with double grand final",
tournamentId: 0, tournamentId: 0,
type: "double_elimination", type: "double_elimination",
settings: { seeding: [1, 2, 3, 4, 5, 6, 7, 8],
size: 8, settings: {},
},
}); });
}); });
@@ -132,37 +129,22 @@ describe("Special cases", () => {
expect(bracket.match(0).opponent2).toBe(null); 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", () => { test("should throw if the participant count of a stage is less than two", () => {
expect(() => expect(() =>
bracket.create({ bracket.create({
name: "Example", name: "Example",
tournamentId: 0, tournamentId: 0,
type: "single_elimination", type: "single_elimination",
settings: { size: 0 }, seeding: [],
}), }),
).toThrow( ).toThrow("Impossible to create a stage with less than 2 participants.");
"Impossible to create an empty stage. If you want an empty seeding, just set the size of the stage.",
);
expect(() => expect(() =>
bracket.create({ bracket.create({
name: "Example", name: "Example",
tournamentId: 0, tournamentId: 0,
type: "single_elimination", type: "single_elimination",
settings: { size: 1 }, seeding: [1],
}), }),
).toThrow("Impossible to create a stage with less than 2 participants."); ).toThrow("Impossible to create a stage with less than 2 participants.");
}); });
@@ -218,9 +200,7 @@ describe("Reset match", () => {
tournamentId: 0, tournamentId: 0,
type: "single_elimination", type: "single_elimination",
seeding: [1, null, null, null, null, null, null, 2], seeding: [1, null, null, null, null, null, null, 2],
settings: { settings: {},
size: 8,
},
}); });
bracket.updateMatch({ bracket.updateMatch({

View File

@@ -8,7 +8,6 @@ import type {
ParticipantResult, ParticipantResult,
ParticipantSlot, ParticipantSlot,
Result, Result,
Seeding,
SeedOrdering, SeedOrdering,
Side, Side,
StageData, StageData,
@@ -294,27 +293,6 @@ export function ensureNoDuplicates<T>(array: (T | null)[]): void {
throw new Error("The seeding has a duplicate participant."); 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. * Ensures that the participant count is valid.
* *
@@ -325,11 +303,6 @@ export function ensureValidSize(
stageType: StageType, stageType: StageType,
participantCount: number, participantCount: number,
): void { ): 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) if (participantCount < 2)
throw Error("Impossible to create a stage with less than 2 participants."); throw Error("Impossible to create a stage with less than 2 participants.");

View File

@@ -417,7 +417,7 @@ export class Propagator {
roundNumber: number, roundNumber: number,
): MatchData[] { ): MatchData[] {
if (stage.type === "single_elimination") if (stage.type === "single_elimination")
return this.getPreviousMatchesFinalSingleElimination(match, stage); return this.getPreviousMatchesFinalSingleElimination(match);
return this.getPreviousMatchesFinalDoubleElimination(match, roundNumber); 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). * Gets the matches leading to the given match, which is in a final group (consolation final).
* *
* @param match The current match. * @param match The current match.
* @param stage The parent stage.
*/ */
private getPreviousMatchesFinalSingleElimination( private getPreviousMatchesFinalSingleElimination(
match: MatchData, match: MatchData,
stage: StageData,
): MatchData[] { ): MatchData[] {
const upperBracket = this.getUpperBracket(match.stage_id); const upperBracket = this.getUpperBracket(match.stage_id);
const upperBracketRoundCount = helpers.getUpperBracketRoundCount( const upperBracketRoundCount = helpers.getUpperBracketRoundCount(
stage.settings.size!, this.participantCount(match.stage_id),
); );
const semiFinalsRound = this.store.selectFirst("round", { const semiFinalsRound = this.store.selectFirst("round", {
@@ -648,7 +646,7 @@ export class Propagator {
const roundNumberLB = roundNumber > 1 ? (roundNumber - 1) * 2 : 1; 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 method = helpers.getLoserOrdering(participantCount, roundNumberLB);
const actualMatchNumberLB = helpers.findLoserMatchNumber( const actualMatchNumberLB = helpers.findLoserMatchNumber(
participantCount, participantCount,
@@ -860,6 +858,26 @@ export class Propagator {
return winnerBracket; 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. * 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). * TournamentStage.settings today (the old brackets-model StageSettings).
*/ */
export interface StageSettings { export interface StageSettings {
// xxx: why not inferred?
/** The number of participants. */
size?: number;
/** Number of groups in a round-robin stage. */ /** Number of groups in a round-robin stage. */
groupCount?: number; groupCount?: number;
@@ -230,15 +226,10 @@ export interface CreateBracketInput {
/** /**
* Engine-internal variant of {@link CreateBracketInput}: settings are the * Engine-internal variant of {@link CreateBracketInput}: settings are the
* already-resolved internal {@link StageSettings} and seeding may be omitted * already-resolved internal {@link StageSettings}.
* in favor of `settings.size` (TBD slots).
*/ */
export interface ResolvedCreateBracketInput export interface ResolvedCreateBracketInput
extends Omit< extends Omit<CreateBracketInput, "settings" | "independentRounds"> {
CreateBracketInput,
"seeding" | "settings" | "independentRounds"
> {
seeding?: Seeding;
settings: StageSettings; settings: StageSettings;
} }

View File

@@ -22,9 +22,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({
id: 1429, id: 1429,
name: "Beta Bracket", name: "Beta Bracket",
number: 2, number: 2,
settings: { settings: {},
size: 16,
},
tournament_id: 815, tournament_id: 815,
type: "double_elimination", type: "double_elimination",
createdAt: 1734286247, createdAt: 1734286247,
@@ -33,9 +31,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({
id: 1430, id: 1430,
name: "Alpha Bracket", name: "Alpha Bracket",
number: 3, number: 3,
settings: { settings: {},
size: 16,
},
tournament_id: 815, tournament_id: 815,
type: "double_elimination", type: "double_elimination",
createdAt: 1734286249, createdAt: 1734286249,
@@ -44,9 +40,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({
id: 1431, id: 1431,
name: "Gamma Bracket", name: "Gamma Bracket",
number: 4, number: 4,
settings: { settings: {},
size: 16,
},
tournament_id: 815, tournament_id: 815,
type: "double_elimination", type: "double_elimination",
createdAt: 1734286340, createdAt: 1734286340,

View File

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

View File

@@ -10,7 +10,6 @@ export const PADDLING_POOL_257 = () =>
number: 1, number: 1,
settings: { settings: {
groupCount: 9, groupCount: 9,
size: 35,
}, },
tournament_id: 27, tournament_id: 27,
type: "round_robin", type: "round_robin",
@@ -6822,7 +6821,6 @@ export const PADDLING_POOL_255 = () =>
number: 1, number: 1,
settings: { settings: {
groupCount: 9, groupCount: 9,
size: 35,
}, },
tournament_id: 18, tournament_id: 18,
type: "round_robin", type: "round_robin",
@@ -14210,9 +14208,7 @@ export const IN_THE_ZONE_32 = ({
id: 16, id: 16,
name: "Main bracket", name: "Main bracket",
number: 1, number: 1,
settings: { settings: {},
size: 32,
},
tournament_id: 11, tournament_id: 11,
type: "double_elimination", type: "double_elimination",
}, },