mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-11 05:36:10 -05:00
Remove stuff
This commit is contained in:
@@ -285,21 +285,16 @@ function generateBracketData(
|
||||
name: "Test Bracket",
|
||||
type: "swiss",
|
||||
seeding: teamIds,
|
||||
settings: {
|
||||
swiss: { groupCount: 1, roundCount: 5 },
|
||||
},
|
||||
settings: { groupCount: 1, roundCount: 5 },
|
||||
});
|
||||
}
|
||||
|
||||
const settings =
|
||||
format === "single_elimination"
|
||||
? { consolationFinal: false }
|
||||
? { thirdPlaceMatch: false }
|
||||
: format === "double_elimination"
|
||||
? {}
|
||||
: {
|
||||
groupCount: Math.ceil(teamIds.length / 4),
|
||||
seedOrdering: ["groups.seed_optimized" as const],
|
||||
};
|
||||
? null
|
||||
: { teamsPerGroup: 4 };
|
||||
|
||||
return Engine.create({
|
||||
tournamentId: 1,
|
||||
|
||||
@@ -68,13 +68,13 @@ export const action: ActionFunction = async ({ params, request }) => {
|
||||
const groupCount = new Set(bracket.data.round.map((r) => r.group_id))
|
||||
.size;
|
||||
|
||||
const settings = tournament.bracketManagerSettings(
|
||||
bracket.settings,
|
||||
bracket.type,
|
||||
seeding.length,
|
||||
);
|
||||
const hasThirdPlaceMatch = Engine.hasThirdPlaceMatch({
|
||||
type: bracket.type,
|
||||
settings: bracket.settings,
|
||||
participantsCount: seeding.length,
|
||||
});
|
||||
|
||||
const maps = settings.consolationFinal
|
||||
const maps = hasThirdPlaceMatch
|
||||
? adjustLinkedRounds({
|
||||
maps: data.maps,
|
||||
thirdPlaceMatchLinked: data.thirdPlaceMatchLinked,
|
||||
@@ -107,7 +107,8 @@ export const action: ActionFunction = async ({ params, request }) => {
|
||||
name: bracket.name,
|
||||
type: bracket.type,
|
||||
seeding,
|
||||
settings,
|
||||
settings: bracket.settings,
|
||||
independentRounds: tournament.isLeagueDivision,
|
||||
abDivisions,
|
||||
});
|
||||
|
||||
@@ -219,11 +220,12 @@ export const action: ActionFunction = async ({ params, request }) => {
|
||||
"Bracket has started, preparing maps no longer possible",
|
||||
);
|
||||
|
||||
const hasThirdPlaceMatch = tournament.bracketManagerSettings(
|
||||
bracket.settings,
|
||||
bracket.type,
|
||||
data.eliminationTeamCount ?? (bracket.seeding ?? []).length,
|
||||
).consolationFinal;
|
||||
const hasThirdPlaceMatch = Engine.hasThirdPlaceMatch({
|
||||
type: bracket.type,
|
||||
settings: bracket.settings,
|
||||
participantsCount:
|
||||
data.eliminationTeamCount ?? (bracket.seeding ?? []).length,
|
||||
});
|
||||
|
||||
await TournamentRepository.upsertPreparedMaps({
|
||||
bracketIdx: data.bracketIdx,
|
||||
|
||||
@@ -40,6 +40,7 @@ import { calendarEditPage } from "~/utils/urls";
|
||||
import { SendouButton } from "../../../components/elements/Button";
|
||||
import { logger } from "../../../utils/logger";
|
||||
import type { Bracket } from "../core/Bracket";
|
||||
import * as Engine from "../core/engine";
|
||||
import * as PreparedMaps from "../core/PreparedMaps";
|
||||
import { getRounds } from "../core/rounds";
|
||||
import type { Tournament } from "../core/Tournament";
|
||||
@@ -99,11 +100,11 @@ export function BracketMapListDialog({
|
||||
const [thirdPlaceMatchLinked, setThirdPlaceMatchLinked] = React.useState(
|
||||
() => {
|
||||
if (
|
||||
!tournament.bracketManagerSettings(
|
||||
bracket.settings,
|
||||
bracket.type,
|
||||
eliminationTeamCount ?? 2,
|
||||
).consolationFinal
|
||||
!Engine.hasThirdPlaceMatch({
|
||||
type: bracket.type,
|
||||
settings: bracket.settings,
|
||||
participantsCount: eliminationTeamCount ?? 2,
|
||||
})
|
||||
) {
|
||||
return true; // default to true if not applicable or elimination team count not yet set (initial state)
|
||||
}
|
||||
|
||||
@@ -103,10 +103,8 @@ describe("swiss standings - losses against tied", () => {
|
||||
type: "swiss",
|
||||
seeding: [1, 2, 3],
|
||||
settings: {
|
||||
swiss: {
|
||||
groupCount: 1,
|
||||
roundCount: 5,
|
||||
},
|
||||
groupCount: 1,
|
||||
roundCount: 5,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -241,7 +239,6 @@ describe("round robin standings - dropped out teams", () => {
|
||||
seeding: [1, 2, 3, 4],
|
||||
settings: {
|
||||
groupCount: 1,
|
||||
seedOrdering: ["groups.seed_optimized"],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -418,7 +415,6 @@ describe("round robin A/B divisions standings", () => {
|
||||
settings: {
|
||||
groupCount: 1,
|
||||
hasAbDivisions: true,
|
||||
seedOrdering: ["groups.seed_optimized"],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -717,7 +713,7 @@ describe("double elimination standings - projected ties", () => {
|
||||
tournamentId: 1,
|
||||
type: "double_elimination",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
settings: { seedOrdering: ["natural"] },
|
||||
settings: {},
|
||||
});
|
||||
|
||||
const groupId = (number: number) =>
|
||||
|
||||
@@ -293,14 +293,12 @@ export abstract class Bracket {
|
||||
const virtualTournamentId = 1;
|
||||
|
||||
if (teams.length >= TOURNAMENT.ENOUGH_TEAMS_TO_START) {
|
||||
const settings = this.tournament.bracketManagerSettings(
|
||||
this.settings,
|
||||
this.type,
|
||||
teams.length,
|
||||
);
|
||||
const abDivisions =
|
||||
this.type === "round_robin" && this.settings?.hasAbDivisions === true
|
||||
? this.abDivisionsForPreview(teams, settings.groupCount)
|
||||
? this.abDivisionsForPreview(
|
||||
teams,
|
||||
Engine.roundRobinGroupCount(this.settings, teams.length),
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return Engine.create({
|
||||
@@ -309,11 +307,12 @@ export abstract class Bracket {
|
||||
type: this.type,
|
||||
seeding: teams,
|
||||
settings: abDivisions
|
||||
? settings
|
||||
? this.settings
|
||||
: {
|
||||
...settings,
|
||||
...this.settings,
|
||||
hasAbDivisions: false,
|
||||
},
|
||||
independentRounds: this.tournament.isLeagueDivision,
|
||||
abDivisions,
|
||||
});
|
||||
}
|
||||
@@ -323,10 +322,8 @@ export abstract class Bracket {
|
||||
|
||||
private abDivisionsForPreview(
|
||||
teams: number[],
|
||||
groupCount: number | undefined,
|
||||
groupCount: number,
|
||||
): (0 | 1)[] | undefined {
|
||||
if (!groupCount) return undefined;
|
||||
|
||||
const assignments = teams.map((teamId) => {
|
||||
const team = this.tournament.teamById(teamId);
|
||||
return team?.abDivision ?? null;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { TournamentStageSettings } from "~/db/tables";
|
||||
import { Tournament } from "~/features/tournament-bracket/core/Tournament";
|
||||
import {
|
||||
LOW_INK_AUGUST_2025,
|
||||
@@ -17,12 +18,12 @@ const Swiss = {
|
||||
tournamentId: number;
|
||||
name: string;
|
||||
seeding: number[];
|
||||
settings?: Engine.StageSettings;
|
||||
settings?: TournamentStageSettings;
|
||||
}) =>
|
||||
Engine.create({
|
||||
...args,
|
||||
type: "swiss",
|
||||
settings: args.settings ?? {},
|
||||
settings: args.settings ?? null,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -62,10 +63,8 @@ describe("Swiss", () => {
|
||||
const data = Swiss.create(
|
||||
createArgsWithDefaults({
|
||||
settings: {
|
||||
swiss: {
|
||||
groupCount: 1,
|
||||
roundCount: 4,
|
||||
},
|
||||
groupCount: 1,
|
||||
roundCount: 4,
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -77,10 +76,8 @@ describe("Swiss", () => {
|
||||
const data = Swiss.create(
|
||||
createArgsWithDefaults({
|
||||
settings: {
|
||||
swiss: {
|
||||
groupCount: 2,
|
||||
roundCount: 5,
|
||||
},
|
||||
groupCount: 2,
|
||||
roundCount: 5,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
} from "~/features/tournament/tournament-utils";
|
||||
import type {
|
||||
Match,
|
||||
Stage,
|
||||
TournamentManagerDataSet,
|
||||
} from "~/features/tournament-bracket/core/engine/types";
|
||||
import type * as Progression from "~/features/tournament-bracket/core/Progression";
|
||||
@@ -347,11 +346,7 @@ export class Tournament {
|
||||
"round_robin" | "swiss"
|
||||
>,
|
||||
seeding: candidateTeams,
|
||||
settings: this.bracketManagerSettings(
|
||||
settings,
|
||||
bracket.type,
|
||||
candidateTeams.length,
|
||||
),
|
||||
settings,
|
||||
}).match;
|
||||
const replays: [number, number][] = [];
|
||||
for (const match of matches) {
|
||||
@@ -483,60 +478,6 @@ export class Tournament {
|
||||
);
|
||||
}
|
||||
|
||||
// xxx: could all of this be internal to Engine (except user input)
|
||||
/** Provides settings for the brackets-manager module with our selected defaults */
|
||||
bracketManagerSettings(
|
||||
selectedSettings: TournamentStageSettings | null,
|
||||
type: Tables["TournamentStage"]["type"],
|
||||
participantsCount: number,
|
||||
): Stage["settings"] {
|
||||
switch (type) {
|
||||
case "single_elimination": {
|
||||
if (participantsCount < 4) {
|
||||
return { consolationFinal: false };
|
||||
}
|
||||
|
||||
return {
|
||||
consolationFinal:
|
||||
selectedSettings?.thirdPlaceMatch ??
|
||||
TOURNAMENT.SE_DEFAULT_HAS_THIRD_PLACE_MATCH,
|
||||
};
|
||||
}
|
||||
case "double_elimination": {
|
||||
return {};
|
||||
}
|
||||
case "round_robin": {
|
||||
const teamsPerGroup =
|
||||
selectedSettings?.teamsPerGroup ??
|
||||
TOURNAMENT.RR_DEFAULT_TEAM_COUNT_PER_GROUP;
|
||||
|
||||
return {
|
||||
groupCount: Math.ceil(participantsCount / teamsPerGroup),
|
||||
seedOrdering: ["groups.seed_optimized"],
|
||||
hasAbDivisions: selectedSettings?.hasAbDivisions ?? false,
|
||||
...(this.isLeagueDivision ? { independentRounds: true } : {}),
|
||||
};
|
||||
}
|
||||
case "swiss": {
|
||||
return {
|
||||
swiss:
|
||||
selectedSettings?.groupCount && selectedSettings.roundCount
|
||||
? {
|
||||
groupCount: selectedSettings.groupCount,
|
||||
roundCount: selectedSettings.roundCount,
|
||||
}
|
||||
: {
|
||||
groupCount: TOURNAMENT.SWISS_DEFAULT_GROUP_COUNT,
|
||||
roundCount: TOURNAMENT.SWISS_DEFAULT_ROUND_COUNT,
|
||||
},
|
||||
};
|
||||
}
|
||||
default: {
|
||||
assertUnreachable(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Is tournament ranked (affects SP/Skill). For tournament to be ranked the organizer needs to enable it and it needs to fit the conditions e.g. it needs to happen when a ranked season is active. */
|
||||
get ranked() {
|
||||
return tournamentIsRanked({
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import * as helpers from "../helpers";
|
||||
import type {
|
||||
BracketData,
|
||||
CreateBracketInput,
|
||||
Duel,
|
||||
GroupData,
|
||||
MatchData,
|
||||
ParticipantSlot,
|
||||
ResolvedCreateBracketInput,
|
||||
RoundData,
|
||||
Seeding,
|
||||
SeedOrdering,
|
||||
@@ -15,7 +15,6 @@ import type {
|
||||
} from "../types";
|
||||
import { MatchStatus } from "../types";
|
||||
import {
|
||||
balanceByes,
|
||||
defaultMinorOrdering,
|
||||
ordering,
|
||||
padSeedingToPowerOfTwo,
|
||||
@@ -28,13 +27,12 @@ import {
|
||||
* being written to storage.
|
||||
*/
|
||||
export class StageCreator {
|
||||
readonly input: CreateBracketInput;
|
||||
readonly input: ResolvedCreateBracketInput;
|
||||
settings: StageSettings;
|
||||
seeding: Seeding | undefined;
|
||||
readonly seedOrdering: SeedOrdering[];
|
||||
readonly data: BracketData;
|
||||
|
||||
constructor(input: CreateBracketInput) {
|
||||
constructor(input: ResolvedCreateBracketInput) {
|
||||
this.input = input;
|
||||
this.settings = structuredClone(input.settings) ?? {};
|
||||
const seeding = input.seeding ? [...input.seeding] : undefined;
|
||||
@@ -42,7 +40,6 @@ export class StageCreator {
|
||||
seeding && input.type !== "round_robin"
|
||||
? padSeedingToPowerOfTwo(seeding)
|
||||
: seeding;
|
||||
this.seedOrdering = this.settings.seedOrdering || [];
|
||||
this.data = { stage: [], group: [], round: [], match: [] };
|
||||
|
||||
if (!input.name) throw Error("You must provide a name for the stage.");
|
||||
@@ -359,9 +356,6 @@ export class StageCreator {
|
||||
helpers.ensureNoDuplicates(this.seeding);
|
||||
this.seeding = helpers.fixSeeding(this.seeding, size);
|
||||
|
||||
if (this.input.type !== "round_robin" && this.settings.balanceByes)
|
||||
this.seeding = balanceByes(this.seeding, this.settings.size);
|
||||
|
||||
return this.getSlotsUsingIds(this.seeding, positions);
|
||||
}
|
||||
|
||||
@@ -389,69 +383,22 @@ export class StageCreator {
|
||||
return positions.map((position) => slots[position - 1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely gets an ordering by its index in the stage input settings.
|
||||
*/
|
||||
getOrdering(
|
||||
orderingIndex: number,
|
||||
stageType: "elimination" | "groups",
|
||||
defaultMethod: SeedOrdering,
|
||||
): SeedOrdering {
|
||||
if (!this.settings.seedOrdering) {
|
||||
this.seedOrdering.push(defaultMethod);
|
||||
return defaultMethod;
|
||||
}
|
||||
|
||||
const method = this.settings.seedOrdering[orderingIndex];
|
||||
if (!method) {
|
||||
this.seedOrdering.push(defaultMethod);
|
||||
return defaultMethod;
|
||||
}
|
||||
|
||||
if (stageType === "elimination" && method.match(/^groups\./))
|
||||
throw Error(
|
||||
"You must specify a seed ordering method without a 'groups' prefix",
|
||||
);
|
||||
|
||||
if (
|
||||
stageType === "groups" &&
|
||||
method !== "natural" &&
|
||||
!method.match(/^groups\./)
|
||||
)
|
||||
throw Error(
|
||||
"You must specify a seed ordering method with a 'groups' prefix",
|
||||
);
|
||||
|
||||
return method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ordering method for the groups in a round-robin stage.
|
||||
*/
|
||||
getRoundRobinOrdering(): SeedOrdering {
|
||||
return this.getOrdering(0, "groups", "groups.effort_balanced");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ordering method for the first round of the upper bracket of an elimination stage.
|
||||
*/
|
||||
getStandardBracketFirstRoundOrdering(): SeedOrdering {
|
||||
return this.getOrdering(0, "elimination", "space_between");
|
||||
return "space_between";
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely gets the only major ordering for the lower bracket.
|
||||
* The only major ordering for the lower bracket.
|
||||
*/
|
||||
private getMajorOrdering(participantCount: number): SeedOrdering {
|
||||
return this.getOrdering(
|
||||
1,
|
||||
"elimination",
|
||||
defaultMinorOrdering[participantCount]?.[0] || "natural",
|
||||
);
|
||||
return defaultMinorOrdering[participantCount]?.[0] || "natural";
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely gets a minor ordering for the lower bracket by its index.
|
||||
* A minor ordering for the lower bracket by its index.
|
||||
*/
|
||||
private getMinorOrdering(
|
||||
participantCount: number,
|
||||
@@ -461,11 +408,7 @@ export class StageCreator {
|
||||
// No ordering for the last minor round. There is only one participant to order.
|
||||
if (index === minorRoundCount - 1) return undefined;
|
||||
|
||||
return this.getOrdering(
|
||||
2 + index,
|
||||
"elimination",
|
||||
defaultMinorOrdering[participantCount]?.[1 + index] || "natural",
|
||||
);
|
||||
return defaultMinorOrdering[participantCount]?.[1 + index] || "natural";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -485,18 +428,4 @@ export class StageCreator {
|
||||
|
||||
return stage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the seed ordering list is stored even if it was not given in the first place.
|
||||
*/
|
||||
ensureSeedOrdering(): void {
|
||||
if (this.settings.seedOrdering?.length === this.seedOrdering.length) return;
|
||||
|
||||
const stage = this.data.stage[0];
|
||||
|
||||
stage.settings = {
|
||||
...stage.settings,
|
||||
seedOrdering: this.seedOrdering,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ describe("Create double elimination stage", () => {
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
|
||||
settings: { seedOrdering: ["natural"] },
|
||||
settings: {},
|
||||
});
|
||||
|
||||
const stage = bracket.stage();
|
||||
@@ -41,7 +41,7 @@ describe("Create double elimination stage", () => {
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
settings: { seedOrdering: ["natural"] },
|
||||
settings: {},
|
||||
});
|
||||
|
||||
expect(bracket.groups().length).toBe(3);
|
||||
|
||||
@@ -10,12 +10,6 @@ import { ordering } from "./seeding";
|
||||
* between the winner of both brackets.
|
||||
*/
|
||||
export function createDoubleElimination(creator: StageCreator): void {
|
||||
if (
|
||||
Array.isArray(creator.settings.seedOrdering) &&
|
||||
creator.settings.seedOrdering.length < 1
|
||||
)
|
||||
throw Error("You must specify at least one seed ordering method.");
|
||||
|
||||
const slots = creator.getSlots();
|
||||
const stage = creator.createStage();
|
||||
const method = creator.getStandardBracketFirstRoundOrdering();
|
||||
|
||||
@@ -1,17 +1,41 @@
|
||||
import type { CreateBracketInput, CreatedBracket } from "../types";
|
||||
import type {
|
||||
CreateBracketInput,
|
||||
CreatedBracket,
|
||||
ResolvedCreateBracketInput,
|
||||
} from "../types";
|
||||
import { StageCreator } from "./builder";
|
||||
import { createDoubleElimination } from "./double-elimination";
|
||||
import { createRoundRobin } from "./round-robin";
|
||||
import { resolveStageSettings } from "./settings";
|
||||
import { createSingleElimination } from "./single-elimination";
|
||||
import { createSwiss } from "./swiss";
|
||||
|
||||
/**
|
||||
* Generates the full structure for a new bracket of any type. Pure function:
|
||||
* returns rows with local ids (0..n-1 per table); the repository maps them to
|
||||
* real row ids on insert. For swiss this includes the empty future rounds +
|
||||
* round 1 matches.
|
||||
* Generates the full structure for a new bracket of any type from the
|
||||
* user-selected settings. Pure function: returns rows with local ids
|
||||
* (0..n-1 per table); the repository maps them to real row ids on insert.
|
||||
* For swiss this includes the empty future rounds + round 1 matches.
|
||||
*/
|
||||
export function create(input: CreateBracketInput): CreatedBracket {
|
||||
return createResolved({
|
||||
tournamentId: input.tournamentId,
|
||||
name: input.name,
|
||||
type: input.type,
|
||||
seeding: input.seeding,
|
||||
settings: resolveStageSettings(input),
|
||||
abDivisions: input.abDivisions,
|
||||
number: input.number,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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`).
|
||||
*/
|
||||
export function createResolved(
|
||||
input: ResolvedCreateBracketInput,
|
||||
): CreatedBracket {
|
||||
if (input.type === "swiss") return createSwiss(input);
|
||||
|
||||
const creator = new StageCreator(input);
|
||||
@@ -30,8 +54,5 @@ export function create(input: CreateBracketInput): CreatedBracket {
|
||||
throw Error("Unknown stage type.");
|
||||
}
|
||||
|
||||
// xxx: hmm, what?
|
||||
creator.ensureSeedOrdering();
|
||||
|
||||
return creator.data;
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ describe("Create a round-robin stage", () => {
|
||||
tournamentId: 0,
|
||||
type: "round_robin",
|
||||
seeding: [1, 2, 3, 4, 5],
|
||||
settings: { groupCount: 2, seedOrdering: ["groups.seed_optimized"] },
|
||||
settings: { groupCount: 2 },
|
||||
});
|
||||
|
||||
const isRealMatch = (match: {
|
||||
@@ -160,15 +160,14 @@ describe("Create a round-robin stage", () => {
|
||||
expect(bracket.matches().length).toBe(4 * 3 * 2);
|
||||
});
|
||||
|
||||
test("should create a round-robin stage with effort balanced", () => {
|
||||
test("should order the groups with snake seeding", () => {
|
||||
bracket.create({
|
||||
name: "Example with effort balanced",
|
||||
name: "Example with snake seeding",
|
||||
tournamentId: 0,
|
||||
type: "round_robin",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
settings: {
|
||||
groupCount: 2,
|
||||
seedOrdering: ["groups.seed_optimized"],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -195,7 +194,6 @@ describe("Create a round-robin stage", () => {
|
||||
settings: {
|
||||
groupCount: 0,
|
||||
size: 4,
|
||||
seedOrdering: ["groups.seed_optimized"],
|
||||
},
|
||||
}),
|
||||
).toThrow("You must provide a strictly positive group count.");
|
||||
@@ -218,7 +216,6 @@ describe("Create a round-robin stage", () => {
|
||||
settings: {
|
||||
groupCount: 1,
|
||||
hasAbDivisions: true,
|
||||
seedOrdering: ["groups.seed_optimized"],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -271,7 +268,6 @@ describe("Create a round-robin stage", () => {
|
||||
settings: {
|
||||
groupCount: 1,
|
||||
hasAbDivisions: true,
|
||||
seedOrdering: ["groups.seed_optimized"],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -65,15 +65,11 @@ function getRoundRobinGroups(creator: StageCreator): ParticipantSlot[][] {
|
||||
return helpers.makeGroups(slots, creator.settings.groupCount);
|
||||
}
|
||||
|
||||
if (
|
||||
Array.isArray(creator.settings.seedOrdering) &&
|
||||
creator.settings.seedOrdering.length !== 1
|
||||
)
|
||||
throw Error("You must specify one seed ordering method.");
|
||||
|
||||
const method = creator.getRoundRobinOrdering();
|
||||
const slots = creator.getSlots();
|
||||
const ordered = ordering[method](slots, creator.settings.groupCount);
|
||||
const ordered = ordering["groups.seed_optimized"](
|
||||
slots,
|
||||
creator.settings.groupCount,
|
||||
);
|
||||
return helpers.makeGroups(ordered, creator.settings.groupCount);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// https://web.archive.org/web/20200601102344/https://tl.net/forum/sc2-tournaments/202139-superior-double-elimination-losers-bracket-seeding
|
||||
|
||||
// xxx: some can probably be removed
|
||||
|
||||
import invariant from "~/utils/invariant";
|
||||
import type { OrderingMap, Seeding, SeedOrdering } from "../types";
|
||||
|
||||
@@ -50,61 +48,6 @@ export const ordering: OrderingMap = {
|
||||
});
|
||||
}
|
||||
},
|
||||
inner_outer: <T>(array: T[]) => {
|
||||
if (array.length === 2) return array;
|
||||
|
||||
const size = array.length / 4;
|
||||
|
||||
const innerPart = [
|
||||
array.slice(size, 2 * size),
|
||||
array.slice(2 * size, 3 * size),
|
||||
]; // [_, X, X, _]
|
||||
const outerPart = [array.slice(0, size), array.slice(3 * size, 4 * size)]; // [X, _, _, X]
|
||||
|
||||
const methods = {
|
||||
inner(part: T[][]): T[] {
|
||||
return [part[0].pop()!, part[1].shift()!];
|
||||
},
|
||||
outer(part: T[][]): T[] {
|
||||
return [part[0].shift()!, part[1].pop()!];
|
||||
},
|
||||
};
|
||||
|
||||
const result: T[] = [];
|
||||
|
||||
/**
|
||||
* Adds a part (inner or outer) of a part.
|
||||
*
|
||||
* @param part The part to process.
|
||||
* @param method The method to use.
|
||||
*/
|
||||
function add(part: T[][], method: "inner" | "outer"): void {
|
||||
if (part[0].length > 0 && part[1].length > 0)
|
||||
result.push(...methods[method](part));
|
||||
}
|
||||
|
||||
for (let i = 0; i < size / 2; i++) {
|
||||
add(outerPart, "outer"); // Outer part's outer
|
||||
add(innerPart, "inner"); // Inner part's inner
|
||||
add(outerPart, "inner"); // Outer part's inner
|
||||
add(innerPart, "outer"); // Inner part's outer
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
"groups.effort_balanced": <T>(array: T[], groupCount: number) => {
|
||||
const result: T[] = [];
|
||||
let i = 0;
|
||||
let j = 0;
|
||||
|
||||
while (result.length < array.length) {
|
||||
result.push(array[i]);
|
||||
i += groupCount;
|
||||
if (i >= array.length) i = ++j;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
"groups.seed_optimized": <T>(array: T[], groupCount: number) => {
|
||||
const groups = Array.from(Array(groupCount), (_): T[] => []);
|
||||
|
||||
@@ -120,9 +63,6 @@ export const ordering: OrderingMap = {
|
||||
|
||||
return groups.flat();
|
||||
},
|
||||
"groups.bracket_optimized": () => {
|
||||
throw Error("Not implemented.");
|
||||
},
|
||||
};
|
||||
|
||||
export const defaultMinorOrdering: { [key: number]: SeedOrdering[] } = {
|
||||
@@ -143,38 +83,6 @@ export const defaultMinorOrdering: { [key: number]: SeedOrdering[] } = {
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Balances BYEs to prevents having BYE against BYE in matches.
|
||||
*
|
||||
* @param seeding The seeding of the stage.
|
||||
* @param participantCount The number of participants in the stage.
|
||||
*/
|
||||
export function balanceByes(
|
||||
seeding: Seeding,
|
||||
participantCount?: number,
|
||||
): Seeding {
|
||||
const nonNullSeeding = seeding.filter((v) => v !== null);
|
||||
const size = participantCount || getNearestPowerOfTwo(nonNullSeeding.length);
|
||||
|
||||
if (nonNullSeeding.length < size / 2) {
|
||||
const flat = nonNullSeeding.flatMap((v) => [v, null]);
|
||||
return setArraySize(flat, size, null);
|
||||
}
|
||||
|
||||
const nonNullCount = nonNullSeeding.length;
|
||||
const nullCount = size - nonNullCount;
|
||||
const againstEachOther = nonNullSeeding
|
||||
.slice(0, nonNullCount - nullCount)
|
||||
.filter((_, i) => i % 2 === 0)
|
||||
.map((_, i) => [nonNullSeeding[2 * i], nonNullSeeding[2 * i + 1]]);
|
||||
const againstNull = nonNullSeeding
|
||||
.slice(nonNullCount - nullCount, nonNullCount)
|
||||
.map((v) => [v, null]);
|
||||
const flat = [...againstEachOther.flat(), ...againstNull.flat()];
|
||||
|
||||
return setArraySize(flat, size, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pads the seeding with BYEs (`null`) until its length is a power of two.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { TournamentStageSettings } from "~/db/tables";
|
||||
import { TOURNAMENT } from "~/features/tournament/tournament-constants";
|
||||
import { assertUnreachable } from "~/utils/types";
|
||||
import type { CreateBracketInput, StageSettings, StageType } from "../types";
|
||||
|
||||
/**
|
||||
* Resolves the user-selected settings into the engine's internal stage
|
||||
* settings, applying our defaults (seed ordering, group counts etc.).
|
||||
*/
|
||||
export function resolveStageSettings(input: CreateBracketInput): StageSettings {
|
||||
const { type, settings, seeding } = input;
|
||||
|
||||
switch (type) {
|
||||
case "single_elimination": {
|
||||
return {
|
||||
consolationFinal: hasThirdPlaceMatch({
|
||||
type,
|
||||
settings,
|
||||
participantsCount: seeding.length,
|
||||
}),
|
||||
};
|
||||
}
|
||||
case "double_elimination": {
|
||||
return {};
|
||||
}
|
||||
case "round_robin": {
|
||||
return {
|
||||
groupCount: roundRobinGroupCount(settings, seeding.length),
|
||||
hasAbDivisions: settings?.hasAbDivisions ?? false,
|
||||
...(input.independentRounds ? { independentRounds: true } : {}),
|
||||
};
|
||||
}
|
||||
case "swiss": {
|
||||
return {
|
||||
swiss:
|
||||
settings?.groupCount && settings.roundCount
|
||||
? {
|
||||
groupCount: settings.groupCount,
|
||||
roundCount: settings.roundCount,
|
||||
}
|
||||
: {
|
||||
groupCount: TOURNAMENT.SWISS_DEFAULT_GROUP_COUNT,
|
||||
roundCount: TOURNAMENT.SWISS_DEFAULT_ROUND_COUNT,
|
||||
},
|
||||
};
|
||||
}
|
||||
default: {
|
||||
assertUnreachable(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the bracket will include a third place match. Only possible for single elimination with at least 4 participants. */
|
||||
export function hasThirdPlaceMatch(args: {
|
||||
type: StageType;
|
||||
settings: TournamentStageSettings | null;
|
||||
participantsCount: number;
|
||||
}): boolean {
|
||||
if (args.type !== "single_elimination") return false;
|
||||
if (args.participantsCount < 4) return false;
|
||||
|
||||
return (
|
||||
args.settings?.thirdPlaceMatch ??
|
||||
TOURNAMENT.SE_DEFAULT_HAS_THIRD_PLACE_MATCH
|
||||
);
|
||||
}
|
||||
|
||||
/** How many groups a round robin bracket will have, derived from the user-selected teams per group count and the participant count. */
|
||||
export function roundRobinGroupCount(
|
||||
settings: TournamentStageSettings | null,
|
||||
participantsCount: number,
|
||||
): number {
|
||||
const teamsPerGroup =
|
||||
settings?.teamsPerGroup ?? TOURNAMENT.RR_DEFAULT_TEAM_COUNT_PER_GROUP;
|
||||
|
||||
return Math.ceil(participantsCount / teamsPerGroup);
|
||||
}
|
||||
@@ -14,7 +14,7 @@ describe("Create single elimination stage", () => {
|
||||
tournamentId: 0,
|
||||
type: "single_elimination" as const,
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
|
||||
settings: { seedOrdering: ["natural" as const] },
|
||||
settings: {},
|
||||
};
|
||||
|
||||
bracket.create(example);
|
||||
@@ -34,13 +34,13 @@ describe("Create single elimination stage", () => {
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, null, 3, 4, null, null, 7, 8],
|
||||
settings: { seedOrdering: ["natural"] },
|
||||
settings: {},
|
||||
});
|
||||
|
||||
expect(bracket.match(4).opponent1?.id).toBe(1);
|
||||
expect(bracket.match(4).opponent2?.id).toBe(null);
|
||||
expect(bracket.match(5).opponent1).toBe(null);
|
||||
expect(bracket.match(5).opponent2?.id).toBe(null);
|
||||
expect(bracket.match(4).opponent1?.id).toBe(null);
|
||||
expect(bracket.match(4).opponent2?.id).toBe(4);
|
||||
expect(bracket.match(5).opponent1?.id).toBe(7);
|
||||
expect(bracket.match(5).opponent2?.id).toBe(3);
|
||||
});
|
||||
|
||||
test("should create a single elimination stage with consolation final", () => {
|
||||
@@ -49,7 +49,7 @@ describe("Create single elimination stage", () => {
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
settings: { consolationFinal: true, seedOrdering: ["natural"] },
|
||||
settings: { consolationFinal: true },
|
||||
});
|
||||
|
||||
expect(bracket.groups().length).toBe(2);
|
||||
@@ -63,14 +63,14 @@ describe("Create single elimination stage", () => {
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [null, null, null, 4, 5, 6, 7, 8],
|
||||
settings: { consolationFinal: true, seedOrdering: ["natural"] },
|
||||
settings: { consolationFinal: true },
|
||||
});
|
||||
|
||||
expect(bracket.match(4).opponent1).toBe(null);
|
||||
expect(bracket.match(4).opponent2?.id).toBe(4);
|
||||
expect(bracket.match(4).opponent1?.id).toBe(8);
|
||||
expect(bracket.match(4).opponent2?.id).toBe(null);
|
||||
|
||||
// Consolation final
|
||||
expect(bracket.match(7).opponent1).toBe(null);
|
||||
expect(bracket.match(7).opponent1?.id).toBe(null);
|
||||
expect(bracket.match(7).opponent2?.id).toBe(null);
|
||||
});
|
||||
|
||||
@@ -80,7 +80,7 @@ describe("Create single elimination stage", () => {
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
settings: { seedOrdering: ["natural"] },
|
||||
settings: {},
|
||||
});
|
||||
|
||||
expect(bracket.groups().length).toBe(1);
|
||||
|
||||
@@ -8,12 +8,6 @@ import { ordering } from "./seeding";
|
||||
* One bracket and optionally a consolation final between semi-final losers.
|
||||
*/
|
||||
export function createSingleElimination(creator: StageCreator): void {
|
||||
if (
|
||||
Array.isArray(creator.settings.seedOrdering) &&
|
||||
creator.settings.seedOrdering.length !== 1
|
||||
)
|
||||
throw Error("You must specify one seed ordering method.");
|
||||
|
||||
const slots = creator.getSlots();
|
||||
const stage = creator.createStage();
|
||||
const method = creator.getStandardBracketFirstRoundOrdering();
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { TOURNAMENT } from "~/features/tournament/tournament-constants";
|
||||
import { nullFilledArray } from "~/utils/arrays";
|
||||
import invariant from "~/utils/invariant";
|
||||
import type { CreateBracketInput, CreatedBracket, MatchData } from "../types";
|
||||
import type {
|
||||
CreatedBracket,
|
||||
MatchData,
|
||||
ResolvedCreateBracketInput,
|
||||
} from "../types";
|
||||
import { MatchStatus } from "../types";
|
||||
|
||||
/**
|
||||
* Creates a Swiss bracket data set: all rounds up front, matches for round 1 only.
|
||||
* Ported from the old core/Swiss.ts create()/firstRoundMatches().
|
||||
*/
|
||||
export function createSwiss(input: CreateBracketInput): CreatedBracket {
|
||||
export function createSwiss(input: ResolvedCreateBracketInput): CreatedBracket {
|
||||
const swissSettings = input.settings?.swiss;
|
||||
|
||||
const groupCount =
|
||||
@@ -56,7 +60,7 @@ function firstRoundMatches({
|
||||
groupCount,
|
||||
roundCount,
|
||||
}: {
|
||||
seeding: CreateBracketInput["seeding"];
|
||||
seeding: ResolvedCreateBracketInput["seeding"];
|
||||
groupCount: number;
|
||||
roundCount: number;
|
||||
}): MatchData[] {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { beforeEach, describe, expect, test } from "vitest";
|
||||
import { createResolved } from "./create";
|
||||
import * as Engine from "./index";
|
||||
import { EngineBracket } from "./test-utils";
|
||||
|
||||
@@ -15,7 +16,7 @@ describe("BYE handling", () => {
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
seeding: [1, null, null, null],
|
||||
settings: { seedOrdering: ["natural"] },
|
||||
settings: {},
|
||||
});
|
||||
|
||||
expect(bracket.match(2).opponent1?.id).toBe(1);
|
||||
@@ -38,28 +39,6 @@ describe("BYE handling", () => {
|
||||
type: "double_elimination",
|
||||
seeding: [1, 2],
|
||||
settings: {
|
||||
seedOrdering: ["natural"],
|
||||
balanceByes: false, // Default value.
|
||||
size: 4,
|
||||
},
|
||||
});
|
||||
|
||||
expect(bracket.match(0).opponent1?.id).toBe(1);
|
||||
expect(bracket.match(0).opponent2?.id).toBe(2);
|
||||
|
||||
expect(bracket.match(1).opponent1).toBe(null);
|
||||
expect(bracket.match(1).opponent2).toBe(null);
|
||||
});
|
||||
|
||||
test("should balance BYEs in the seeding", () => {
|
||||
bracket.create({
|
||||
name: "Example with BYEs",
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
seeding: [1, 2],
|
||||
settings: {
|
||||
seedOrdering: ["natural"],
|
||||
balanceByes: true,
|
||||
size: 4,
|
||||
},
|
||||
});
|
||||
@@ -82,7 +61,6 @@ describe("Position checks", () => {
|
||||
type: "double_elimination",
|
||||
settings: {
|
||||
size: 8,
|
||||
seedOrdering: ["natural"],
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -102,7 +80,7 @@ describe("Position checks", () => {
|
||||
test("should have a position where we need the origin of a participant", () => {
|
||||
const matchFromWbRound1 = bracket.match(0);
|
||||
expect(matchFromWbRound1.opponent1?.position).toBe(1);
|
||||
expect(matchFromWbRound1.opponent2?.position).toBe(2);
|
||||
expect(matchFromWbRound1.opponent2?.position).toBe(8);
|
||||
|
||||
const matchFromLbRound1 = bracket.match(7);
|
||||
expect(matchFromLbRound1.opponent1?.position).toBe(1);
|
||||
@@ -123,21 +101,21 @@ describe("Special cases", () => {
|
||||
|
||||
test("should throw if the name of the stage is not provided", () => {
|
||||
expect(() =>
|
||||
Engine.create({
|
||||
createResolved({
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
settings: {},
|
||||
} as Engine.CreateBracketInput),
|
||||
} as Engine.ResolvedCreateBracketInput),
|
||||
).toThrow("You must provide a name for the stage.");
|
||||
});
|
||||
|
||||
test("should throw if the tournament id of the stage is not provided", () => {
|
||||
expect(() =>
|
||||
Engine.create({
|
||||
createResolved({
|
||||
name: "Example",
|
||||
type: "single_elimination",
|
||||
settings: {},
|
||||
} as Engine.CreateBracketInput),
|
||||
} as Engine.ResolvedCreateBracketInput),
|
||||
).toThrow("You must provide a tournament id for the stage.");
|
||||
});
|
||||
|
||||
@@ -147,11 +125,11 @@ describe("Special cases", () => {
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7],
|
||||
settings: { seedOrdering: ["natural"] },
|
||||
settings: {},
|
||||
});
|
||||
|
||||
expect(bracket.match(3).opponent1?.id).toBe(7);
|
||||
expect(bracket.match(3).opponent2).toBe(null);
|
||||
expect(bracket.match(0).opponent1?.id).toBe(1);
|
||||
expect(bracket.match(0).opponent2).toBe(null);
|
||||
});
|
||||
|
||||
test("should throw if the size of a stage is not a power of two", () => {
|
||||
@@ -199,15 +177,7 @@ describe("Seeding and ordering in elimination", () => {
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
|
||||
settings: {
|
||||
seedOrdering: [
|
||||
"inner_outer",
|
||||
"reverse",
|
||||
"pair_flip",
|
||||
"half_shift",
|
||||
"reverse",
|
||||
],
|
||||
},
|
||||
settings: {},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -217,8 +187,8 @@ describe("Seeding and ordering in elimination", () => {
|
||||
expect(firstRoundMatchWB.opponent2?.position).toBe(16);
|
||||
|
||||
const firstRoundMatchLB = bracket.match(15);
|
||||
expect(firstRoundMatchLB.opponent1?.position).toBe(8);
|
||||
expect(firstRoundMatchLB.opponent2?.position).toBe(7);
|
||||
expect(firstRoundMatchLB.opponent1?.position).toBe(1);
|
||||
expect(firstRoundMatchLB.opponent2?.position).toBe(2);
|
||||
|
||||
const secondRoundMatchLB = bracket.match(19);
|
||||
expect(secondRoundMatchLB.opponent1?.position).toBe(2);
|
||||
@@ -240,13 +210,15 @@ describe("Reset match", () => {
|
||||
});
|
||||
|
||||
test("should reset results of a match", () => {
|
||||
// Seeds 1 and 2 are placed into the same first-round match (positions 1
|
||||
// and 8) so that match 0 is a real two-team match under the default
|
||||
// space_between ordering, while the rest of the bracket is BYEs.
|
||||
bracket.create({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2],
|
||||
seeding: [1, null, null, null, null, null, null, 2],
|
||||
settings: {
|
||||
seedOrdering: ["natural"],
|
||||
size: 8,
|
||||
},
|
||||
});
|
||||
@@ -292,9 +264,7 @@ describe("Reset match", () => {
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2, 3, 4],
|
||||
settings: {
|
||||
seedOrdering: ["natural"],
|
||||
},
|
||||
settings: {},
|
||||
});
|
||||
|
||||
bracket.updateMatch({
|
||||
@@ -321,12 +291,12 @@ describe("Reset match", () => {
|
||||
|
||||
describe("Engine data immutability", () => {
|
||||
test("engine operations return new data and leave the input untouched", () => {
|
||||
const initial = Engine.create({
|
||||
const initial = createResolved({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2, 3, 4],
|
||||
settings: { seedOrdering: ["natural"] },
|
||||
settings: {},
|
||||
});
|
||||
|
||||
const snapshot = structuredClone(initial);
|
||||
@@ -347,12 +317,12 @@ describe("Engine data immutability", () => {
|
||||
});
|
||||
|
||||
test("changedMatches contains only genuinely changed rows", () => {
|
||||
const initial = Engine.create({
|
||||
const initial = createResolved({
|
||||
name: "Example",
|
||||
tournamentId: 0,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2, 3, 4],
|
||||
settings: { seedOrdering: ["natural"] },
|
||||
settings: {},
|
||||
});
|
||||
|
||||
const afterReport = Engine.reportResult(initial, {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { balanceByes, ordering } from "./create/seeding";
|
||||
import { ordering } from "./create/seeding";
|
||||
import {
|
||||
assertAbDivisionRoundRobin,
|
||||
assertRoundRobin,
|
||||
@@ -288,32 +288,6 @@ describe("A/B division group distribution", () => {
|
||||
});
|
||||
|
||||
describe("Seed ordering methods", () => {
|
||||
test("should place 2 participants with inner-outer method", () => {
|
||||
const teams = [1, 2];
|
||||
const placement = ordering.inner_outer(teams);
|
||||
expect(placement).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
test("should place 4 participants with inner-outer method", () => {
|
||||
const teams = [1, 2, 3, 4];
|
||||
const placement = ordering.inner_outer(teams);
|
||||
expect(placement).toEqual([1, 4, 2, 3]);
|
||||
});
|
||||
|
||||
test("should place 8 participants with inner-outer method", () => {
|
||||
const teams = [1, 2, 3, 4, 5, 6, 7, 8];
|
||||
const placement = ordering.inner_outer(teams);
|
||||
expect(placement).toEqual([1, 8, 4, 5, 2, 7, 3, 6]);
|
||||
});
|
||||
|
||||
test("should place 16 participants with inner-outer method", () => {
|
||||
const teams = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
|
||||
const placement = ordering.inner_outer(teams);
|
||||
expect(placement).toEqual([
|
||||
1, 16, 8, 9, 4, 13, 5, 12, 2, 15, 7, 10, 3, 14, 6, 11,
|
||||
]);
|
||||
});
|
||||
|
||||
test("should make a natural ordering", () => {
|
||||
expect(ordering.natural([1, 2, 3, 4, 5, 6, 7, 8])).toEqual([
|
||||
1, 2, 3, 4, 5, 6, 7, 8,
|
||||
@@ -344,23 +318,6 @@ describe("Seed ordering methods", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("should make an effort balanced ordering for groups", () => {
|
||||
expect(
|
||||
ordering["groups.effort_balanced"]([1, 2, 3, 4, 5, 6, 7, 8], 4),
|
||||
).toEqual([1, 5, 2, 6, 3, 7, 4, 8]);
|
||||
|
||||
expect(
|
||||
ordering["groups.effort_balanced"](
|
||||
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
|
||||
4,
|
||||
),
|
||||
).toEqual([1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15, 4, 8, 12, 16]);
|
||||
|
||||
expect(
|
||||
ordering["groups.effort_balanced"]([1, 2, 3, 4, 5, 6, 7, 8], 2),
|
||||
).toEqual([1, 3, 5, 7, 2, 4, 6, 8]);
|
||||
});
|
||||
|
||||
test("should make a snake ordering for groups", () => {
|
||||
expect(
|
||||
ordering["groups.seed_optimized"]([1, 2, 3, 4, 5, 6, 7, 8], 4),
|
||||
@@ -378,132 +335,3 @@ describe("Seed ordering methods", () => {
|
||||
).toEqual([1, 4, 5, 8, 2, 3, 6, 7]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Balance BYEs", () => {
|
||||
test("should ignore input BYEs in the seeding", () => {
|
||||
expect(
|
||||
balanceByes(
|
||||
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, null, null, null, null],
|
||||
16,
|
||||
),
|
||||
).toEqual(balanceByes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 16));
|
||||
|
||||
expect(
|
||||
balanceByes(
|
||||
[1, 2, 3, null, 4, 5, 6, 7, 8, null, 9, 10, null, 11, null, 12, null],
|
||||
16,
|
||||
),
|
||||
).toEqual(balanceByes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 16));
|
||||
});
|
||||
|
||||
test("should take the target size as an argument or calculate it", () => {
|
||||
expect(balanceByes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 16)).toEqual(
|
||||
balanceByes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
|
||||
);
|
||||
});
|
||||
|
||||
test("should prefer matches with only one BYE", () => {
|
||||
expect(
|
||||
balanceByes([
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
]),
|
||||
).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, null, 10, null, 11, null, 12, null]);
|
||||
|
||||
expect(
|
||||
balanceByes(
|
||||
[
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
],
|
||||
16,
|
||||
),
|
||||
).toEqual([
|
||||
1,
|
||||
null,
|
||||
2,
|
||||
null,
|
||||
3,
|
||||
null,
|
||||
4,
|
||||
null,
|
||||
5,
|
||||
null,
|
||||
6,
|
||||
null,
|
||||
7,
|
||||
null,
|
||||
8,
|
||||
null,
|
||||
]);
|
||||
|
||||
expect(
|
||||
balanceByes(
|
||||
[
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
],
|
||||
16,
|
||||
),
|
||||
).toEqual([
|
||||
1,
|
||||
null,
|
||||
2,
|
||||
null,
|
||||
3,
|
||||
null,
|
||||
4,
|
||||
null,
|
||||
5,
|
||||
null,
|
||||
6,
|
||||
null,
|
||||
7,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ordering } from "./create/seeding";
|
||||
import { defaultMinorOrdering, ordering } from "./create/seeding";
|
||||
import type {
|
||||
DeepPartial,
|
||||
Duel,
|
||||
@@ -1067,15 +1067,14 @@ function getLoserRoundLoserCount(
|
||||
/**
|
||||
* Returns the ordering method of a round of a loser bracket.
|
||||
*
|
||||
* @param seedOrdering The list of seed orderings.
|
||||
* @param participantCount The number of participants in the stage.
|
||||
* @param roundNumber Number of the round.
|
||||
*/
|
||||
export function getLoserOrdering(
|
||||
seedOrdering: SeedOrdering[],
|
||||
participantCount: number,
|
||||
roundNumber: number,
|
||||
): SeedOrdering | undefined {
|
||||
const orderingIndex = 1 + Math.floor(roundNumber / 2);
|
||||
return seedOrdering[orderingIndex];
|
||||
return defaultMinorOrdering[participantCount]?.[Math.floor(roundNumber / 2)];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
export { create } from "./create";
|
||||
export { hasThirdPlaceMatch, roundRobinGroupCount } from "./create/settings";
|
||||
export { endDroppedTeamMatches } from "./propagation/dropped-teams";
|
||||
export { reportResult } from "./propagation/report-result";
|
||||
export { resetMatchResults } from "./propagation/reset-result";
|
||||
|
||||
@@ -15,7 +15,7 @@ describe("Previous and next match update in double elimination stage", () => {
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
|
||||
settings: { seedOrdering: ["natural"] },
|
||||
settings: {},
|
||||
});
|
||||
|
||||
const before = bracket.match(8); // First match of WB round 2
|
||||
@@ -191,55 +191,51 @@ describe("Previous and next match update in double elimination stage", () => {
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
|
||||
settings: {
|
||||
seedOrdering: ["natural", "reverse", "reverse"],
|
||||
},
|
||||
settings: {},
|
||||
});
|
||||
|
||||
bracket.updateMatch({ id: 0, opponent1: { result: "win" } }); // WB 1.1
|
||||
expect(
|
||||
bracket.match(18).opponent2?.id, // Determined opponent for last match of LB round 1 (reverse ordering for losers)
|
||||
bracket.match(15).opponent1?.id, // Determined opponent for first match of LB round 1 (natural ordering for losers)
|
||||
).toBe(bracket.match(0).opponent2?.id); // Loser of first match round 1
|
||||
|
||||
bracket.updateMatch({ id: 1, opponent1: { result: "win" } }); // WB 1.2
|
||||
expect(
|
||||
bracket.match(18).opponent1?.id, // Determined opponent for last match of LB round 1 (reverse ordering for losers)
|
||||
bracket.match(15).opponent2?.id, // Determined opponent for first match of LB round 1 (natural ordering for losers)
|
||||
).toBe(bracket.match(1).opponent2?.id); // Loser of second match round 1
|
||||
|
||||
bracket.updateMatch({ id: 8, opponent1: { result: "win" } }); // WB 2.1
|
||||
expect(
|
||||
bracket.match(22).opponent1?.id, // Determined opponent for last match of LB round 2 (reverse ordering for losers)
|
||||
bracket.match(20).opponent1?.id, // Determined opponent for first match of LB round 2
|
||||
).toBe(bracket.match(8).opponent2?.id); // Loser of first match round 2
|
||||
|
||||
bracket.updateMatch({ id: 6, opponent1: { result: "win" } }); // WB 1.7
|
||||
bracket.updateMatch({ id: 7, opponent1: { result: "win" } }); // WB 1.8
|
||||
bracket.updateMatch({ id: 11, opponent1: { result: "win" } }); // WB 2.4
|
||||
bracket.updateMatch({ id: 15, opponent1: { result: "win" } }); // LB 1.1
|
||||
bracket.updateMatch({ id: 19, opponent1: { result: "win" } }); // LB 2.1
|
||||
bracket.updateMatch({ id: 18, opponent1: { result: "win" } }); // LB 1.4
|
||||
|
||||
expect(bracket.match(8).status).toBe(TournamentMatchStatus.Completed); // WB 2.1
|
||||
});
|
||||
|
||||
test("should send the losers to the right LB matches in round 1", () => {
|
||||
bracket.create({
|
||||
name: "Example with inner_outer loser ordering",
|
||||
name: "Example with natural loser ordering",
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
settings: {
|
||||
seedOrdering: ["inner_outer", "inner_outer"],
|
||||
},
|
||||
settings: {},
|
||||
});
|
||||
|
||||
expect(bracket.match(7).opponent1?.position).toBe(1);
|
||||
expect(bracket.match(7).opponent2?.position).toBe(4);
|
||||
expect(bracket.match(8).opponent1?.position).toBe(2);
|
||||
expect(bracket.match(8).opponent2?.position).toBe(3);
|
||||
expect(bracket.match(7).opponent2?.position).toBe(2);
|
||||
expect(bracket.match(8).opponent1?.position).toBe(3);
|
||||
expect(bracket.match(8).opponent2?.position).toBe(4);
|
||||
|
||||
// Match of position 1.
|
||||
bracket.updateMatch({
|
||||
id: 0,
|
||||
opponent1: { result: "win" }, // Loser id: 7.
|
||||
opponent1: { result: "win" }, // Loser id: 8.
|
||||
});
|
||||
|
||||
expect(bracket.match(7).opponent1?.id).toBe(8);
|
||||
@@ -247,25 +243,25 @@ describe("Previous and next match update in double elimination stage", () => {
|
||||
// Match of position 2.
|
||||
bracket.updateMatch({
|
||||
id: 1,
|
||||
opponent1: { result: "win" }, // Loser id: 4.
|
||||
opponent1: { result: "win" }, // Loser id: 5.
|
||||
});
|
||||
|
||||
expect(bracket.match(8).opponent1?.id).toBe(5);
|
||||
expect(bracket.match(7).opponent2?.id).toBe(5);
|
||||
|
||||
// Match of position 3.
|
||||
bracket.updateMatch({
|
||||
id: 2,
|
||||
opponent1: { result: "win" }, // Loser id: 6.
|
||||
opponent1: { result: "win" }, // Loser id: 7.
|
||||
});
|
||||
|
||||
expect(bracket.match(8).opponent2?.id).toBe(7);
|
||||
expect(bracket.match(8).opponent1?.id).toBe(7);
|
||||
|
||||
// Match of position 4.
|
||||
bracket.updateMatch({
|
||||
id: 3,
|
||||
opponent1: { result: "win" }, // Loser id: 5.
|
||||
opponent1: { result: "win" }, // Loser id: 6.
|
||||
});
|
||||
|
||||
expect(bracket.match(7).opponent2?.id).toBe(6);
|
||||
expect(bracket.match(8).opponent2?.id).toBe(6);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,9 +4,8 @@ import { Propagator } from "./traversal";
|
||||
|
||||
/**
|
||||
* Ends all unfinished matches involving dropped teams by awarding wins to
|
||||
* their opponents. Ported from tournament-utils.server.ts. `randomPick` is
|
||||
* injected so the engine stays deterministic/testable (caller passes a
|
||||
* Math.random-backed impl in production).
|
||||
* their opponents. Ported from tournament-utils.server.ts. When both teams in
|
||||
* a match have dropped, a random winner is picked.
|
||||
*
|
||||
* Matches that only gain a dropped opponent through propagation caused by this
|
||||
* call are not ended (same as the old implementation, which iterated a
|
||||
@@ -14,14 +13,11 @@ import { Propagator } from "./traversal";
|
||||
*/
|
||||
export function endDroppedTeamMatches(
|
||||
data: BracketData,
|
||||
args: {
|
||||
droppedTeamIds: number[];
|
||||
randomPick: (a: number, b: number) => number;
|
||||
},
|
||||
droppedTeamIds: number[],
|
||||
): DroppedTeamsResult {
|
||||
const store = new Store(data);
|
||||
const propagator = new Propagator(store);
|
||||
const droppedTeamIds = new Set(args.droppedTeamIds);
|
||||
const droppedTeamIdsSet = new Set(droppedTeamIds);
|
||||
|
||||
const endedMatchIds: number[] = [];
|
||||
|
||||
@@ -30,15 +26,15 @@ export function endDroppedTeamMatches(
|
||||
if (match.opponent1.result === "win" || match.opponent2.result === "win")
|
||||
continue;
|
||||
|
||||
const team1Dropped = droppedTeamIds.has(match.opponent1.id);
|
||||
const team2Dropped = droppedTeamIds.has(match.opponent2.id);
|
||||
const team1Dropped = droppedTeamIdsSet.has(match.opponent1.id);
|
||||
const team2Dropped = droppedTeamIdsSet.has(match.opponent2.id);
|
||||
|
||||
if (!team1Dropped && !team2Dropped) continue;
|
||||
|
||||
const winnerTeamId = (() => {
|
||||
if (team1Dropped && !team2Dropped) return match.opponent2.id;
|
||||
if (!team1Dropped && team2Dropped) return match.opponent1.id;
|
||||
return args.randomPick(match.opponent1.id, match.opponent2.id);
|
||||
return Math.random() < 0.5 ? match.opponent1.id : match.opponent2.id;
|
||||
})();
|
||||
|
||||
const stored = store.select("match", match.id);
|
||||
|
||||
@@ -649,10 +649,7 @@ export class Propagator {
|
||||
const roundNumberLB = roundNumber > 1 ? (roundNumber - 1) * 2 : 1;
|
||||
|
||||
const participantCount = stage.settings.size!;
|
||||
const method = helpers.getLoserOrdering(
|
||||
stage.settings.seedOrdering!,
|
||||
roundNumberLB,
|
||||
);
|
||||
const method = helpers.getLoserOrdering(participantCount, roundNumberLB);
|
||||
const actualMatchNumberLB = helpers.findLoserMatchNumber(
|
||||
participantCount,
|
||||
roundNumberLB,
|
||||
|
||||
@@ -9,7 +9,7 @@ const example = {
|
||||
tournamentId: 0,
|
||||
type: "double_elimination" as const,
|
||||
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
|
||||
settings: { seedOrdering: ["natural" as const] },
|
||||
settings: {},
|
||||
};
|
||||
|
||||
describe("Update matches", () => {
|
||||
@@ -82,7 +82,7 @@ describe("Update matches", () => {
|
||||
|
||||
const nextMatch = bracket.match(8);
|
||||
expect(nextMatch.status).toBe(TournamentMatchStatus.Waiting);
|
||||
expect(nextMatch.opponent1?.id).toBe(2);
|
||||
expect(nextMatch.opponent1?.id).toBe(16);
|
||||
});
|
||||
|
||||
test("should update the status of the next match", () => {
|
||||
@@ -208,7 +208,7 @@ describe("Give opponent IDs when updating", () => {
|
||||
tournamentId: 0,
|
||||
type: "double_elimination",
|
||||
seeding: [1, 2, 3, 4],
|
||||
settings: { seedOrdering: ["natural"] },
|
||||
settings: {},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -216,7 +216,7 @@ describe("Give opponent IDs when updating", () => {
|
||||
bracket.updateMatch({
|
||||
id: 0,
|
||||
opponent1: {
|
||||
id: 2,
|
||||
id: 4,
|
||||
score: 10,
|
||||
},
|
||||
opponent2: {
|
||||
@@ -235,7 +235,7 @@ describe("Give opponent IDs when updating", () => {
|
||||
bracket.updateMatch({
|
||||
id: 0,
|
||||
opponent1: {
|
||||
id: 2,
|
||||
id: 4,
|
||||
score: 10,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
// Test-only harness giving the pure engine a stateful surface similar to the
|
||||
// old BracketsManager, so the vendored test suite could be ported 1:1.
|
||||
|
||||
import { createResolved } from "./create";
|
||||
import * as Engine from "./index";
|
||||
import type {
|
||||
BracketData,
|
||||
CreateBracketInput,
|
||||
GroupData,
|
||||
MatchData,
|
||||
ParticipantResult,
|
||||
ResolvedCreateBracketInput,
|
||||
RoundData,
|
||||
StageData,
|
||||
} from "./types";
|
||||
@@ -23,10 +24,13 @@ export class EngineBracket {
|
||||
data: BracketData | undefined;
|
||||
|
||||
create(
|
||||
input: Omit<CreateBracketInput, "settings"> &
|
||||
Partial<Pick<CreateBracketInput, "settings">>,
|
||||
input: Omit<ResolvedCreateBracketInput, "settings"> &
|
||||
Partial<Pick<ResolvedCreateBracketInput, "settings">>,
|
||||
): void {
|
||||
const created = Engine.create({ ...input, settings: input.settings ?? {} });
|
||||
const created = createResolved({
|
||||
...input,
|
||||
settings: input.settings ?? {},
|
||||
});
|
||||
|
||||
if (!this.data) {
|
||||
this.data = created;
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { Tables, TournamentRoundMaps } from "~/db/tables";
|
||||
import type {
|
||||
Tables,
|
||||
TournamentRoundMaps,
|
||||
TournamentStageSettings,
|
||||
} from "~/db/tables";
|
||||
|
||||
/**
|
||||
* Match/set outcome for one side. Upstream brackets-model also had "draw" —
|
||||
@@ -21,18 +25,14 @@ export type GroupType =
|
||||
| "loser_bracket"
|
||||
| "final_group";
|
||||
|
||||
// xxx: what can we delete here?
|
||||
export type SeedOrdering =
|
||||
| "natural"
|
||||
| "reverse"
|
||||
| "half_shift"
|
||||
| "reverse_half_shift"
|
||||
| "pair_flip"
|
||||
| "inner_outer"
|
||||
| "space_between"
|
||||
| "groups.effort_balanced"
|
||||
| "groups.seed_optimized"
|
||||
| "groups.bracket_optimized";
|
||||
| "groups.seed_optimized";
|
||||
|
||||
/** The seeding for a stage. Each element is a participant id or a BYE: `null`. */
|
||||
export type Seeding = (number | null)[];
|
||||
@@ -62,23 +62,6 @@ export interface StageSettings {
|
||||
/** The number of participants. */
|
||||
size?: number;
|
||||
|
||||
/**
|
||||
* A list of ordering methods to apply to the seeding.
|
||||
*
|
||||
* - For a round-robin stage: 1 item required (**with** `"groups."` prefix).
|
||||
* - For a simple elimination stage, 1 item required (**without** `"groups."` prefix).
|
||||
* - For a double elimination stage, 1 item required, 3+ items supported (**without** `"groups."` prefix).
|
||||
* - Item 1 (required) - Used to distribute in WB round 1.
|
||||
* - Item 2 - Used to distribute WB losers in LB round 1.
|
||||
* - Items 3+ - Used to distribute WB losers in LB minor rounds (1 per round).
|
||||
*/
|
||||
// xxx: make implementation detail?
|
||||
seedOrdering?: SeedOrdering[];
|
||||
|
||||
/** Whether to balance BYEs in the seeding of an elimination stage. */
|
||||
// xxx: make implementation detail?
|
||||
balanceByes?: boolean;
|
||||
|
||||
/** Number of groups in a round-robin stage. */
|
||||
groupCount?: number;
|
||||
|
||||
@@ -233,15 +216,32 @@ export interface CreateBracketInput {
|
||||
tournamentId: number;
|
||||
name: string;
|
||||
type: StageType;
|
||||
/** Team ids in seed order; `null` = BYE. When omitted, `settings.size` must be given (TBD slots). */
|
||||
seeding?: Seeding;
|
||||
settings: StageSettings;
|
||||
/** Team ids in seed order; `null` = BYE. */
|
||||
seeding: Seeding;
|
||||
/** User-selected settings; the engine derives its internal stage settings (defaults, group counts, seed ordering) from these. */
|
||||
settings: TournamentStageSettings | null;
|
||||
/** (Round robin only) Whether matches are playable independently of rounds (league divisions). */
|
||||
independentRounds?: boolean;
|
||||
/** Parallel to seeding; required when settings.hasAbDivisions. 0 = A, 1 = B. */
|
||||
abDivisions?: (0 | 1)[];
|
||||
/** Stage number within the tournament. Defaults to 1 (local data; the repository assigns the real number on insert). */
|
||||
number?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
export interface ResolvedCreateBracketInput
|
||||
extends Omit<
|
||||
CreateBracketInput,
|
||||
"seeding" | "settings" | "independentRounds"
|
||||
> {
|
||||
seeding?: Seeding;
|
||||
settings: StageSettings;
|
||||
}
|
||||
|
||||
/** Mirrors the old manager.update.match() partial-update input. */
|
||||
export interface ReportResultInput {
|
||||
matchId: number;
|
||||
@@ -312,6 +312,5 @@ export interface DroppedTeamsResult extends EngineResult {
|
||||
|
||||
// xxx: just rename
|
||||
export type TournamentManagerDataSet = BracketData;
|
||||
export type Stage = StageData;
|
||||
export type Round = RoundData;
|
||||
export type Match = MatchData;
|
||||
|
||||
@@ -24,12 +24,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({
|
||||
number: 2,
|
||||
settings: {
|
||||
size: 16,
|
||||
seedOrdering: [
|
||||
"space_between",
|
||||
"natural",
|
||||
"reverse_half_shift",
|
||||
"reverse",
|
||||
],
|
||||
},
|
||||
tournament_id: 815,
|
||||
type: "double_elimination",
|
||||
@@ -41,12 +35,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({
|
||||
number: 3,
|
||||
settings: {
|
||||
size: 16,
|
||||
seedOrdering: [
|
||||
"space_between",
|
||||
"natural",
|
||||
"reverse_half_shift",
|
||||
"reverse",
|
||||
],
|
||||
},
|
||||
tournament_id: 815,
|
||||
type: "double_elimination",
|
||||
@@ -58,12 +46,6 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({
|
||||
number: 4,
|
||||
settings: {
|
||||
size: 16,
|
||||
seedOrdering: [
|
||||
"space_between",
|
||||
"natural",
|
||||
"reverse_half_shift",
|
||||
"reverse",
|
||||
],
|
||||
},
|
||||
tournament_id: 815,
|
||||
type: "double_elimination",
|
||||
|
||||
@@ -12,7 +12,6 @@ export const SWIM_OR_SINK_167 = (
|
||||
number: 1,
|
||||
settings: {
|
||||
groupCount: 11,
|
||||
seedOrdering: ["groups.seed_optimized"],
|
||||
size: 44,
|
||||
},
|
||||
tournament_id: 672,
|
||||
|
||||
@@ -10,7 +10,6 @@ export const PADDLING_POOL_257 = () =>
|
||||
number: 1,
|
||||
settings: {
|
||||
groupCount: 9,
|
||||
seedOrdering: ["groups.seed_optimized"],
|
||||
size: 35,
|
||||
},
|
||||
tournament_id: 27,
|
||||
@@ -6823,7 +6822,6 @@ export const PADDLING_POOL_255 = () =>
|
||||
number: 1,
|
||||
settings: {
|
||||
groupCount: 9,
|
||||
seedOrdering: ["groups.seed_optimized"],
|
||||
size: 35,
|
||||
},
|
||||
tournament_id: 18,
|
||||
@@ -14214,13 +14212,6 @@ export const IN_THE_ZONE_32 = ({
|
||||
number: 1,
|
||||
settings: {
|
||||
size: 32,
|
||||
seedOrdering: [
|
||||
"space_between",
|
||||
"natural",
|
||||
"reverse",
|
||||
"half_shift",
|
||||
"natural",
|
||||
],
|
||||
},
|
||||
tournament_id: 11,
|
||||
type: "double_elimination",
|
||||
|
||||
@@ -164,14 +164,14 @@ function roundRobinToSingleEliminationTournament() {
|
||||
tournamentId: 1,
|
||||
type: "round_robin",
|
||||
seeding: [1, 2, 3, 4],
|
||||
settings: { groupCount: 1, seedOrdering: ["groups.seed_optimized"] },
|
||||
settings: { groupCount: 1 },
|
||||
});
|
||||
bracket.create({
|
||||
name: "B1",
|
||||
tournamentId: 1,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2],
|
||||
settings: { seedOrdering: ["natural"] },
|
||||
settings: {},
|
||||
});
|
||||
|
||||
// play every match across both brackets, lower id always wins
|
||||
@@ -219,7 +219,7 @@ function singleEliminationTournament() {
|
||||
tournamentId: 1,
|
||||
type: "single_elimination",
|
||||
seeding: [1, 2, 3, 4],
|
||||
settings: { seedOrdering: ["natural"] },
|
||||
settings: {},
|
||||
});
|
||||
|
||||
while (true) {
|
||||
@@ -270,7 +270,6 @@ function abDivisionsTournament() {
|
||||
settings: {
|
||||
groupCount: 1,
|
||||
hasAbDivisions: true,
|
||||
seedOrdering: ["groups.seed_optimized"],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -111,12 +111,6 @@ export async function dbStartTournament(seeding: number[], tournamentId = 1) {
|
||||
|
||||
const bracket = tournament.bracketByIdx(0)!;
|
||||
|
||||
const settings = tournament.bracketManagerSettings(
|
||||
bracket.settings,
|
||||
bracket.type,
|
||||
seeding.length,
|
||||
);
|
||||
|
||||
await BracketRepository.insertBracket({
|
||||
tournamentId: tournament.ctx.id,
|
||||
bracket: Engine.create({
|
||||
@@ -124,7 +118,7 @@ export async function dbStartTournament(seeding: number[], tournamentId = 1) {
|
||||
name: bracket.name,
|
||||
type: bracket.type,
|
||||
seeding,
|
||||
settings,
|
||||
settings: bracket.settings,
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -91,10 +91,7 @@ export function endDroppedTeamMatches({
|
||||
.map((team) => team.id);
|
||||
if (typeof droppedTeamId === "number") droppedTeamIds.push(droppedTeamId);
|
||||
|
||||
const result = Engine.endDroppedTeamMatches(data, {
|
||||
droppedTeamIds,
|
||||
randomPick: (a, b) => (Math.random() < 0.5 ? a : b), // xxx: why randomPick a param like this?
|
||||
});
|
||||
const result = Engine.endDroppedTeamMatches(data, droppedTeamIds);
|
||||
|
||||
// xxx: maybe move to caller? or repository function
|
||||
for (const matchId of result.endedMatchIds) {
|
||||
|
||||
Reference in New Issue
Block a user