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 groupsAtBaseSize = numGroups - groupsWithExtra; const minGroupSize = groupsAtBaseSize > 0 ? teamsPerGroup : teamsPerGroup + 1; const matchesGuaranteed = minGroupSize - 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, }); } } const uniqueConfigs = new Map(); for (const config of configs) { const groupSizes: number[] = []; for (let i = 0; i < config.num_groups; i++) { const size = config.teams_per_group + (i < config.groups_with_extra ? 1 : 0); groupSizes.push(size); } groupSizes.sort((a, b) => b - a); const key = `${groupSizes.join(',')}_advance${config.advance_per_group}`; if (!uniqueConfigs.has(key)) { uniqueConfigs.set(key, config); } } return Array.from(uniqueConfigs.values()).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): 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 && teams_per_group) { 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(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); }