groups init
This commit is contained in:
167
src/features/tournaments/utils/group-config.ts
Normal file
167
src/features/tournaments/utils/group-config.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { GroupConfig } from "../types";
|
||||
|
||||
function isPowerOfTwo(n: number): boolean {
|
||||
return n > 0 && (n & (n - 1)) === 0;
|
||||
}
|
||||
|
||||
function getNextPowerOfTwo(n: number): number {
|
||||
if (n <= 0) return 1;
|
||||
if (isPowerOfTwo(n)) return n;
|
||||
return Math.pow(2, Math.ceil(Math.log2(n)));
|
||||
}
|
||||
|
||||
export interface GroupConfigOption extends GroupConfig {
|
||||
groups_with_extra: number;
|
||||
knockout_size: number;
|
||||
wildcards_needed: number;
|
||||
total_group_matches: number;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export function calculateGroupConfigurations(teamCount: number): GroupConfigOption[] {
|
||||
if (teamCount < 4) {
|
||||
throw new Error("Need at least 4 teams for group stage");
|
||||
}
|
||||
|
||||
const configs: GroupConfigOption[] = [];
|
||||
|
||||
for (let teamsPerGroup = 3; teamsPerGroup <= Math.min(6, teamCount); teamsPerGroup++) {
|
||||
const numGroups = Math.floor(teamCount / teamsPerGroup);
|
||||
const remainder = teamCount % teamsPerGroup;
|
||||
|
||||
if (numGroups < 2) continue;
|
||||
|
||||
if (remainder > numGroups) continue;
|
||||
|
||||
const groupsWithExtra = remainder;
|
||||
const matchesGuaranteed = teamsPerGroup - 1;
|
||||
|
||||
for (let advancePerGroup = 1; advancePerGroup <= Math.min(3, teamsPerGroup - 1); advancePerGroup++) {
|
||||
const teamsAdvancing = numGroups * advancePerGroup;
|
||||
|
||||
if (teamsAdvancing < 4 || teamsAdvancing > 32) continue;
|
||||
|
||||
const knockoutSize = getNextPowerOfTwo(teamsAdvancing);
|
||||
const wildcardsNeeded = knockoutSize - teamsAdvancing;
|
||||
|
||||
if (wildcardsNeeded > teamsAdvancing / 2) continue;
|
||||
|
||||
let totalGroupMatches = 0;
|
||||
for (let i = 0; i < numGroups; i++) {
|
||||
const groupSize = teamsPerGroup + (i < groupsWithExtra ? 1 : 0);
|
||||
totalGroupMatches += (groupSize * (groupSize - 1)) / 2;
|
||||
}
|
||||
|
||||
const description = generateDescription({
|
||||
num_groups: numGroups,
|
||||
teams_per_group: teamsPerGroup,
|
||||
groups_with_extra: groupsWithExtra,
|
||||
advance_per_group: advancePerGroup,
|
||||
matches_guaranteed: matchesGuaranteed,
|
||||
knockout_size: knockoutSize,
|
||||
wildcards_needed: wildcardsNeeded,
|
||||
});
|
||||
|
||||
configs.push({
|
||||
num_groups: numGroups,
|
||||
teams_per_group: teamsPerGroup,
|
||||
advance_per_group: advancePerGroup,
|
||||
matches_guaranteed: matchesGuaranteed,
|
||||
seeding_method: "random",
|
||||
groups_with_extra: groupsWithExtra,
|
||||
knockout_size: knockoutSize,
|
||||
wildcards_needed: wildcardsNeeded,
|
||||
total_group_matches: totalGroupMatches,
|
||||
description,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return configs.sort((a, b) => {
|
||||
if (a.matches_guaranteed !== b.matches_guaranteed) {
|
||||
return b.matches_guaranteed - a.matches_guaranteed;
|
||||
}
|
||||
if (a.wildcards_needed !== b.wildcards_needed) {
|
||||
return a.wildcards_needed - b.wildcards_needed;
|
||||
}
|
||||
if (a.knockout_size !== b.knockout_size) {
|
||||
return b.knockout_size - a.knockout_size;
|
||||
}
|
||||
return b.num_groups - a.num_groups;
|
||||
});
|
||||
}
|
||||
|
||||
function generateDescription(config: Partial<GroupConfigOption>): string {
|
||||
const { num_groups, teams_per_group, groups_with_extra, matches_guaranteed, advance_per_group, knockout_size, wildcards_needed } = config;
|
||||
|
||||
let desc = '';
|
||||
|
||||
if (groups_with_extra && groups_with_extra > 0) {
|
||||
const largerGroupSize = teams_per_group + 1;
|
||||
const smallerGroupCount = num_groups! - groups_with_extra;
|
||||
|
||||
if (smallerGroupCount > 0) {
|
||||
desc += `${groups_with_extra} group${groups_with_extra > 1 ? 's' : ''} of ${largerGroupSize}, `;
|
||||
desc += `${smallerGroupCount} group${smallerGroupCount > 1 ? 's' : ''} of ${teams_per_group}`;
|
||||
} else {
|
||||
desc += `${num_groups} group${num_groups! > 1 ? 's' : ''} of ${largerGroupSize}`;
|
||||
}
|
||||
} else {
|
||||
desc += `${num_groups} group${num_groups! > 1 ? 's' : ''} of ${teams_per_group}`;
|
||||
}
|
||||
|
||||
desc += ` • ${matches_guaranteed} match${matches_guaranteed! > 1 ? 'es' : ''} guaranteed`;
|
||||
desc += ` • Top ${advance_per_group} advance`;
|
||||
|
||||
if (wildcards_needed && wildcards_needed > 0) {
|
||||
desc += ` + ${wildcards_needed} wildcard${wildcards_needed > 1 ? 's' : ''}`;
|
||||
}
|
||||
|
||||
desc += ` → ${knockout_size}-team knockout`;
|
||||
|
||||
return desc;
|
||||
}
|
||||
|
||||
export function assignTeamsToGroups(
|
||||
teamIds: string[],
|
||||
config: GroupConfigOption,
|
||||
seed?: number
|
||||
): string[][] {
|
||||
const shuffled = shuffleArray([...teamIds], seed);
|
||||
|
||||
const groups: string[][] = [];
|
||||
let teamIndex = 0;
|
||||
|
||||
for (let groupIndex = 0; groupIndex < config.num_groups; groupIndex++) {
|
||||
const groupSize = config.teams_per_group + (groupIndex < config.groups_with_extra ? 1 : 0);
|
||||
const groupTeams = shuffled.slice(teamIndex, teamIndex + groupSize);
|
||||
groups.push(groupTeams);
|
||||
teamIndex += groupSize;
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
function shuffleArray<T>(array: T[], seed?: number): T[] {
|
||||
const arr = [...array];
|
||||
const random = seed !== undefined ? seededRandom(seed) : Math.random;
|
||||
|
||||
for (let i = arr.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(random() * (i + 1));
|
||||
[arr[i], arr[j]] = [arr[j], arr[i]];
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
function seededRandom(seed: number): () => number {
|
||||
let value = seed;
|
||||
return () => {
|
||||
value = (value * 9301 + 49297) % 233280;
|
||||
return value / 233280;
|
||||
};
|
||||
}
|
||||
|
||||
export function getGroupName(index: number): string {
|
||||
return String.fromCharCode(65 + index);
|
||||
}
|
||||
Reference in New Issue
Block a user