From 33573af2beb0c1bef6d257e15831525d8e3c6c7b Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Wed, 26 Jun 2024 23:14:36 +0100 Subject: [PATCH 01/41] upd --- src/experiments/generateFull.ts | 15 ++ src/experiments/generateMatchdays.ts | 320 +++++++++++++++++++++++++++ src/experiments/generatePairings.ts | 297 +++++++++++++++++++++++++ src/experiments/groups.ts | 91 ++++++++ src/experiments/index.ts | 11 + src/index.tsx | 4 + 6 files changed, 738 insertions(+) create mode 100644 src/experiments/generateFull.ts create mode 100644 src/experiments/generateMatchdays.ts create mode 100644 src/experiments/generatePairings.ts create mode 100644 src/experiments/groups.ts create mode 100644 src/experiments/index.ts diff --git a/src/experiments/generateFull.ts b/src/experiments/generateFull.ts new file mode 100644 index 00000000..7b537a89 --- /dev/null +++ b/src/experiments/generateFull.ts @@ -0,0 +1,15 @@ +export default (teams: readonly T[], numTimes = 1) => { + const matches: [T, T][] = []; + for (let k = 0; k < numTimes; ++k) { + for (let i = 0; i < teams.length - 1; ++i) { + for (let j = i + 1; j < teams.length; ++j) { + const match: [T, T] = + k & 1 + ? ([teams[j], teams[i]] as const) + : ([teams[i], teams[j]] as const); + matches.push(match); + } + } + } + return matches; +}; diff --git a/src/experiments/generateMatchdays.ts b/src/experiments/generateMatchdays.ts new file mode 100644 index 00000000..a3690fb2 --- /dev/null +++ b/src/experiments/generateMatchdays.ts @@ -0,0 +1,320 @@ +import { chunk, orderBy, shuffle } from 'lodash'; + +import { findFirstSolution } from '../utils/backtrack'; + +import generateFull from './generateFull'; + +const EASY_NUM_MATCHDAYS = 6; + +const generateMatchdays = ({ + teams, + numPots, + numMatchdays, + maxHomeGamesVsPot, +}: { + teams: readonly number[]; + numPots: number; + numMatchdays: number; + maxHomeGamesVsPot: number; +}) => { + const numTeamsPerPot = teams.length / numPots; + const numGamesPerMatchday = teams.length / 2; + + const maxGamesAtHome = Math.ceil(numMatchdays / 2); + + console.log('doing for', numMatchdays); + + const foo = new Set( + numMatchdays > EASY_NUM_MATCHDAYS + ? generateMatchdays({ + teams, + numPots, + numMatchdays: numMatchdays - 1, + maxHomeGamesVsPot, + }).flatMap(md => orderBy(md, m => Math.min(m[0], m[1]))) + : [], + ); + + let remainingGames = generateFull(teams); + + remainingGames = orderBy( + shuffle([ + ...remainingGames, + ...remainingGames.map(([a, b]) => [b, a] as [number, number]), + ]), + [ + // () => Math.random(), + m => (foo.has(m) ? 0 : 1), + ([a, b]) => { + if (a % 2 === 0 && b - a === 1) { + return 0.0000000001 * a; + } + if ((a - b === 3 && b % 4 === 0) || (a - b === 1 && b % 4 === 1)) { + return 0.0001 * a; + } + return Number.POSITIVE_INFINITY; + }, + ([a, b]) => -Math.abs(a - b), + ], + ); + + console.log('initial games', JSON.stringify(remainingGames)); + + const matches: [number, number][] = []; + const numHomeGamesByTeam: Record = {}; + const numAwayGamesByTeam: Record = {}; + + /** + * team:pot:home? + */ + const hasPlayedWithPotMap: Record< + `${number}:${number}:${'h' | 'a'}`, + number + > = {}; + + while (matches.length < numMatchdays * numGamesPerMatchday) { + console.log('nice'); + // remainingGames = shuffle(remainingGames); + + console.log(numHomeGamesByTeam, numAwayGamesByTeam, hasPlayedWithPotMap); + + console.log('orderedRemainingGames', [...remainingGames]); + + // eslint-disable-next-line no-loop-func + const pickedMatch = remainingGames.find(m => { + console.log('test...', m, { + remainingGames: [...remainingGames], + matches: [...matches], + }); + const solution = findFirstSolution( + { + source: remainingGames, + sourceStartFromIndex: 0, + target: matches, + numHomeGamesByTeam, + numAwayGamesByTeam, + hasPlayedWithPotMap, + picked: m, + }, + { + reject: c => { + // console.log('c', c); + const [m1, m2] = c.picked; + const matches = c.target; + + // Ensure the teams play same number of games at home & away + if (c.numHomeGamesByTeam[m1] === maxGamesAtHome) { + return true; + } + if (c.numAwayGamesByTeam[m2] === maxGamesAtHome) { + return true; + } + + // Ensure neither team has already played on this matchday + const matchdayIndex = Math.floor( + matches.length / numGamesPerMatchday, + ); + if ( + (c.numHomeGamesByTeam[m1] ?? 0) + + (c.numAwayGamesByTeam[m1] ?? 0) > + matchdayIndex + ) { + return true; + } + if ( + (c.numHomeGamesByTeam[m2] ?? 0) + + (c.numAwayGamesByTeam[m2] ?? 0) > + matchdayIndex + ) { + return true; + } + + // // Ensure the difference between home & away games is -1, 0 or 1 + // if ( + // (c.numHomeGamesByTeam[m1] ?? 0) > (c.numAwayGamesByTeam[m1] ?? 0) + // ) { + // return true; + // } + + // if ( + // (c.numAwayGamesByTeam[m2] ?? 0) > (c.numHomeGamesByTeam[m2] ?? 0) + // ) { + // return true; + // } + + const homeTeamPotIndex = Math.floor(m1 / numTeamsPerPot); + const awayTeamPotIndex = Math.floor(m2 / numTeamsPerPot); + + if ( + c.hasPlayedWithPotMap[`${m1}:${awayTeamPotIndex}:h`] === + maxHomeGamesVsPot + ) { + return true; + } + + if ( + c.hasPlayedWithPotMap[`${m2}:${homeTeamPotIndex}:a`] === + maxHomeGamesVsPot + ) { + return true; + } + + return false; + }, + accept: c => + c.target.length === numMatchdays * numGamesPerMatchday - 1, + generate: c => { + const newTarget = [...c.target, c.picked]; + const newNumHomeGamesByTeam = { + ...c.numHomeGamesByTeam, + [c.picked[0]]: (c.numHomeGamesByTeam[c.picked[0]] ?? 0) + 1, + } as typeof c.numHomeGamesByTeam; + const newNumAwayGamesByTeam = { + ...c.numAwayGamesByTeam, + [c.picked[1]]: (c.numAwayGamesByTeam[c.picked[1]] ?? 0) + 1, + } as typeof c.numAwayGamesByTeam; + + const pickedHomePotIndex = Math.floor(c.picked[0] / numTeamsPerPot); + const pickedAwayPotIndex = Math.floor(c.picked[1] / numTeamsPerPot); + + const newHasPlayedWithPotMap: typeof c.hasPlayedWithPotMap = { + ...c.hasPlayedWithPotMap, + [`${c.picked[0]}:${pickedAwayPotIndex}:h`]: + (c.hasPlayedWithPotMap[ + `${c.picked[0]}:${pickedAwayPotIndex}:h` + ] ?? 0) + 1, + [`${c.picked[1]}:${pickedHomePotIndex}:a`]: + (c.hasPlayedWithPotMap[ + `${c.picked[1]}:${pickedHomePotIndex}:a` + ] ?? 0) + 1, + } satisfies typeof c.hasPlayedWithPotMap; + + const oldMatchdayIndex = Math.floor( + c.target.length / numGamesPerMatchday, + ); + + const newMatchdayIndex = Math.floor( + newTarget.length / numGamesPerMatchday, + ); + + const newSource = c.source.filter(m => { + if ( + (m[0] === c.picked[0] && m[1] === c.picked[1]) || + (m[0] === c.picked[1] && m[1] === c.picked[0]) + ) { + return false; + } + if (newNumHomeGamesByTeam[m[0]] === maxGamesAtHome) { + return false; + } + if (newNumAwayGamesByTeam[m[1]] === maxGamesAtHome) { + return false; + } + + const homePot = Math.floor(m[0] / numTeamsPerPot); + const awayPot = Math.floor(m[1] / numTeamsPerPot); + + if ( + hasPlayedWithPotMap[`${m[0]}:${awayPot}:h`] === + maxHomeGamesVsPot + ) { + return false; + } + + if ( + hasPlayedWithPotMap[`${m[1]}:${homePot}:a`] === + maxHomeGamesVsPot + ) { + return false; + } + + return true; + }); + + const bannedMatches = c.source.slice(0, c.sourceStartFromIndex); + const bannedMatchesSet = new Set(bannedMatches); + + const candidates: (typeof c)[] = []; + + for (let i = 0; i < newSource.length; ++i) { + const newPicked = newSource[i]; + if (bannedMatchesSet.has(newPicked)) { + continue; + } + candidates.push({ + source: newSource, + sourceStartFromIndex: + newMatchdayIndex === oldMatchdayIndex ? i : 0, + target: newTarget, + picked: newPicked, + numHomeGamesByTeam: newNumHomeGamesByTeam, + numAwayGamesByTeam: newNumAwayGamesByTeam, + hasPlayedWithPotMap: newHasPlayedWithPotMap, + }); + } + + return candidates; + }, + }, + ); + if (!solution) { + console.log('sol', solution); + } + // console.log('sol', solution); + return solution !== undefined; + })!; + console.log('taking', pickedMatch); + + matches.push(pickedMatch); + + numHomeGamesByTeam[pickedMatch[0]] = + (numHomeGamesByTeam[pickedMatch[0]] ?? 0) + 1; + numAwayGamesByTeam[pickedMatch[1]] = + (numAwayGamesByTeam[pickedMatch[1]] ?? 0) + 1; + + const pickedHomePot = Math.floor(pickedMatch[0] / numTeamsPerPot); + const pickedAwayPot = Math.floor(pickedMatch[1] / numTeamsPerPot); + hasPlayedWithPotMap[`${pickedMatch[0]}:${pickedAwayPot}:h`] = + (hasPlayedWithPotMap[`${pickedMatch[0]}:${pickedAwayPot}:h`] ?? 0) + 1; + hasPlayedWithPotMap[`${pickedMatch[1]}:${pickedHomePot}:a`] = + (hasPlayedWithPotMap[`${pickedMatch[1]}:${pickedHomePot}:a`] ?? 0) + 1; + + console.log(numHomeGamesByTeam, numAwayGamesByTeam); + + remainingGames = remainingGames.filter(([a, b]) => { + const justPicked = + (a === pickedMatch[0] && b === pickedMatch[1]) || + (a === pickedMatch[1] && b === pickedMatch[0]); + if (justPicked) { + return false; + } + // if (numHomeGamesByTeam[a] === maxGamesAtHome) { + // return false; + // } + // if (numAwayGamesByTeam[b] === maxGamesAtHome) { + // return false; + // } + + const aPot = Math.floor(a / numTeamsPerPot); + const bPot = Math.floor(b / numTeamsPerPot); + + if (hasPlayedWithPotMap[`${a}:${bPot}:h`] === maxHomeGamesVsPot) { + return false; + } + + if (hasPlayedWithPotMap[`${b}:${aPot}:a`] === maxHomeGamesVsPot) { + return false; + } + + return true; + }); + console.log('new remaining', remainingGames); + console.log('current total', matches.length); + } + + console.log('done for num matchdays:', numMatchdays); + + return chunk(matches, numGamesPerMatchday); +}; + +export default generateMatchdays; diff --git a/src/experiments/generatePairings.ts b/src/experiments/generatePairings.ts new file mode 100644 index 00000000..d9a9af40 --- /dev/null +++ b/src/experiments/generatePairings.ts @@ -0,0 +1,297 @@ +import { orderBy, shuffle } from 'lodash'; + +import { findFirstSolution } from '../utils/backtrack'; + +import generateFull from './generateFull'; + +const generateMatchdays = ({ + teams, + numPots, + numMatchdays, + maxHomeGamesVsPot, +}: { + teams: readonly number[]; + numPots: number; + numMatchdays: number; + maxHomeGamesVsPot: number; +}) => { + const numTeamsPerPot = teams.length / numPots; + const numGamesPerMatchday = teams.length / 2; + + const maxGamesAtHome = Math.ceil(numMatchdays / 2); + + console.log('doing for', numMatchdays); + + let remainingGames = generateFull(teams); + + remainingGames = [ + ...remainingGames, + ...remainingGames.map(([a, b]) => [b, a] as [number, number]), + ]; + + remainingGames = orderBy(remainingGames, [ + m => Math.min(...m), + m => Math.max(...m), + // () => Math.random(), + // ([a, b]) => { + // if (a % 2 === 0 && b - a === 1) { + // return 0.0000000001 * a; + // } + // if ((a - b === 3 && b % 4 === 0) || (a - b === 1 && b % 4 === 1)) { + // return 0.0001 * a; + // } + // return Number.POSITIVE_INFINITY; + // }, + // ([a, b]) => -Math.abs(a - b), + ]); + + // remainingGames = shuffle(remainingGames); + + console.log('initial games', JSON.stringify(remainingGames)); + + const matches: [number, number][] = []; + const numHomeGamesByTeam: Record = {}; + const numAwayGamesByTeam: Record = {}; + + /** + * team:pot:home? + */ + const hasPlayedWithPotMap: Record< + `${number}:${number}:${'h' | 'a'}`, + number + > = {}; + + while (matches.length < numMatchdays * numGamesPerMatchday) { + console.log('nice'); + // remainingGames = shuffle(remainingGames); + + console.log(numHomeGamesByTeam, numAwayGamesByTeam, hasPlayedWithPotMap); + + console.log('orderedRemainingGames', [...remainingGames]); + + // eslint-disable-next-line no-loop-func + const pickedMatch = shuffle(remainingGames).find(m => { + console.log('test...', m, { + remainingGames: [...remainingGames], + matches: [...matches], + }); + const solution = findFirstSolution( + { + source: remainingGames, + target: matches, + numHomeGamesByTeam, + numAwayGamesByTeam, + hasPlayedWithPotMap, + picked: m, + }, + { + reject: c => { + const [m1, m2] = c.picked; + + // Ensure the teams play same number of games at home & away + if (c.numHomeGamesByTeam[m1] === maxGamesAtHome) { + return true; + } + if (c.numAwayGamesByTeam[m2] === maxGamesAtHome) { + return true; + } + + const homeTeamPotIndex = Math.floor(m1 / numTeamsPerPot); + const awayTeamPotIndex = Math.floor(m2 / numTeamsPerPot); + + if ( + c.hasPlayedWithPotMap[`${m1}:${awayTeamPotIndex}:h`] === + maxHomeGamesVsPot + ) { + return true; + } + + if ( + c.hasPlayedWithPotMap[`${m2}:${homeTeamPotIndex}:a`] === + maxHomeGamesVsPot + ) { + return true; + } + + return false; + }, + + accept: c => + c.target.length === numMatchdays * numGamesPerMatchday - 1, + + generate: c => { + const newTarget = [...c.target, c.picked]; + const newNumHomeGamesByTeam = { + ...c.numHomeGamesByTeam, + [c.picked[0]]: (c.numHomeGamesByTeam[c.picked[0]] ?? 0) + 1, + } as typeof c.numHomeGamesByTeam; + const newNumAwayGamesByTeam = { + ...c.numAwayGamesByTeam, + [c.picked[1]]: (c.numAwayGamesByTeam[c.picked[1]] ?? 0) + 1, + } as typeof c.numAwayGamesByTeam; + + const pickedHomePotIndex = Math.floor(c.picked[0] / numTeamsPerPot); + const pickedAwayPotIndex = Math.floor(c.picked[1] / numTeamsPerPot); + + const newHasPlayedWithPotMap: typeof c.hasPlayedWithPotMap = { + ...c.hasPlayedWithPotMap, + [`${c.picked[0]}:${pickedAwayPotIndex}:h`]: + (c.hasPlayedWithPotMap[ + `${c.picked[0]}:${pickedAwayPotIndex}:h` + ] ?? 0) + 1, + [`${c.picked[1]}:${pickedHomePotIndex}:a`]: + (c.hasPlayedWithPotMap[ + `${c.picked[1]}:${pickedHomePotIndex}:a` + ] ?? 0) + 1, + } satisfies typeof c.hasPlayedWithPotMap; + + const newSource = c.source.filter(m => { + if ( + (m[0] === c.picked[0] && m[1] === c.picked[1]) || + (m[0] === c.picked[1] && m[1] === c.picked[0]) + ) { + return false; + } + if (newNumHomeGamesByTeam[m[0]] === maxGamesAtHome) { + return false; + } + if (newNumAwayGamesByTeam[m[1]] === maxGamesAtHome) { + return false; + } + + const homePot = Math.floor(m[0] / numTeamsPerPot); + const awayPot = Math.floor(m[1] / numTeamsPerPot); + + if ( + hasPlayedWithPotMap[`${m[0]}:${awayPot}:h`] === + maxHomeGamesVsPot + ) { + return false; + } + + if ( + hasPlayedWithPotMap[`${m[1]}:${homePot}:a`] === + maxHomeGamesVsPot + ) { + return false; + } + + return true; + }); + + const candidates: (typeof c)[] = []; + + const lowestRemainingTeam = + newSource.length > 0 + ? // eslint-disable-next-line unicorn/no-array-reduce + newSource.reduce( + (prev, cur) => Math.min(prev, ...cur), + Math.min(...newSource[0]), + ) + : undefined; + + if (lowestRemainingTeam !== undefined) { + let nextPot: number | undefined; + let nextPlace: 'h' | 'a' | undefined; + for (let i = 0; i < numPots; ++i) { + if (!newHasPlayedWithPotMap[`${lowestRemainingTeam}:${i}:h`]) { + nextPot = i; + nextPlace = 'h'; + break; + } + if (!newHasPlayedWithPotMap[`${lowestRemainingTeam}:${i}:a`]) { + nextPot = i; + nextPlace = 'a'; + break; + } + } + + for (const newPicked of newSource) { + const homePot = Math.floor(newPicked[0] / numTeamsPerPot); + const awayPot = Math.floor(newPicked[1] / numTeamsPerPot); + + const isMatchGood = + (nextPlace === 'h' && + newPicked[0] === lowestRemainingTeam && + awayPot === nextPot) || + (nextPlace === 'a' && + newPicked[1] === lowestRemainingTeam && + homePot === nextPot); + if (isMatchGood) { + candidates.push({ + source: newSource, + target: newTarget, + picked: newPicked, + numHomeGamesByTeam: newNumHomeGamesByTeam, + numAwayGamesByTeam: newNumAwayGamesByTeam, + hasPlayedWithPotMap: newHasPlayedWithPotMap, + }); + } + } + } + + return candidates; + }, + }, + ); + if (!solution) { + console.log('sol', solution); + } + // console.log('sol', solution); + return solution !== undefined; + })!; + console.log('taking', pickedMatch); + + matches.push(pickedMatch); + + numHomeGamesByTeam[pickedMatch[0]] = + (numHomeGamesByTeam[pickedMatch[0]] ?? 0) + 1; + numAwayGamesByTeam[pickedMatch[1]] = + (numAwayGamesByTeam[pickedMatch[1]] ?? 0) + 1; + + const pickedHomePot = Math.floor(pickedMatch[0] / numTeamsPerPot); + const pickedAwayPot = Math.floor(pickedMatch[1] / numTeamsPerPot); + hasPlayedWithPotMap[`${pickedMatch[0]}:${pickedAwayPot}:h`] = + (hasPlayedWithPotMap[`${pickedMatch[0]}:${pickedAwayPot}:h`] ?? 0) + 1; + hasPlayedWithPotMap[`${pickedMatch[1]}:${pickedHomePot}:a`] = + (hasPlayedWithPotMap[`${pickedMatch[1]}:${pickedHomePot}:a`] ?? 0) + 1; + + console.log(numHomeGamesByTeam, numAwayGamesByTeam); + + remainingGames = remainingGames.filter(([a, b]) => { + const justPicked = + (a === pickedMatch[0] && b === pickedMatch[1]) || + (a === pickedMatch[1] && b === pickedMatch[0]); + if (justPicked) { + return false; + } + if (numHomeGamesByTeam[a] === maxGamesAtHome) { + return false; + } + if (numAwayGamesByTeam[b] === maxGamesAtHome) { + return false; + } + + const aPot = Math.floor(a / numTeamsPerPot); + const bPot = Math.floor(b / numTeamsPerPot); + + if (hasPlayedWithPotMap[`${a}:${bPot}:h`] === maxHomeGamesVsPot) { + return false; + } + + if (hasPlayedWithPotMap[`${b}:${aPot}:a`] === maxHomeGamesVsPot) { + return false; + } + + return true; + }); + console.log('new remaining', remainingGames); + console.log('current total', matches.length); + } + + console.log('done for num matchdays:', numMatchdays); + + return matches; +}; + +export default generateMatchdays; diff --git a/src/experiments/groups.ts b/src/experiments/groups.ts new file mode 100644 index 00000000..0d1cd210 --- /dev/null +++ b/src/experiments/groups.ts @@ -0,0 +1,91 @@ +import { chunk, pull, range, shuffle } from 'lodash'; + +import { findFirstSolution } from '#utils/backtrack.js'; + +const teams = range(36).map(i => ({ id: i + 1 })); +const numPots = 4; + +type Team = (typeof teams)[number]; + +const pots = chunk(teams, teams.length / numPots); + +const pairedPots: Record<`${number}:${number}`, [Team[], Team[]]> = {}; + +const totalMatches = teams.length * numPots; + +for (let i = 0; i < pots.length; ++i) { + for (let j = 0; j < pots.length; ++j) { + pairedPots[`${i}:${j}`] = [pots[i], pots[j]]; + } +} + +const resultingMatches: [Team, Team][] = []; + +while (true) { + for (let i = 0; i < pots.length; ++i) { + for (let j = 0; j < pots.length; ++j) { + const [remainingHomePot, remainingAwayPot] = pairedPots[`${i}:${j}`]; + let isHomeBeingPicked = true; + while (remainingHomePot.length > 0 || remainingAwayPot.length > 0) { + const potToPickFrom = isHomeBeingPicked + ? remainingHomePot + : remainingAwayPot; + + // eslint-disable-next-line no-loop-func + const pickedTeam = potToPickFrom.find(team => { + const solution = findFirstSolution( + { + pairedPots, + matches: resultingMatches, + picked: team, + currentHomePot: i, + currentAwayPot: j, + isHomeBeingPicked, + }, + { + // eslint-disable-next-line arrow-body-style + reject: c => { + return false; + }, + accept: c => c.matches.length === totalMatches - 1, + generate: c => { + const newPotPair = [ + ...c.pairedPots[`${c.currentHomePot}:${c.currentAwayPot}`], + ] as [Team[], Team[]]; + const index = c.isHomeBeingPicked ? 0 : 1; + newPotPair[index] = [...newPotPair[index]].filter( + item => item !== c.picked, + ); + + const newPairedPots = { + ...c.pairedPots, + [`${c.currentHomePot}:${c.currentAwayPot}`]: newPotPair, + } as typeof c.pairedPots; + + const newMatches = [...c.matches]; + if (c.isHomeBeingPicked) { + newMatches.push([c.picked] as unknown as [Team, Team]); + } else { + newMatches[newMatches.length - 1] = [ + newMatches.at(-1)![0], + c.picked, + ]; + } + + const newIsHomeBeingPicked = !c.isHomeBeingPicked + + if () + }, + }, + ); + + return solution !== undefined; + }); + + isHomeBeingPicked = !isHomeBeingPicked; + } + } + } +} + +console.log('resulting matches', resultingMatches); diff --git a/src/experiments/index.ts b/src/experiments/index.ts new file mode 100644 index 00000000..1336c3bb --- /dev/null +++ b/src/experiments/index.ts @@ -0,0 +1,11 @@ +import { range } from 'lodash'; + +import generateMatchdays from './generatePairings'; + +const matchdays = generateMatchdays({ + teams: range(36), + numPots: 4, + numMatchdays: 8, + maxHomeGamesVsPot: 1, +}); +console.log('final', matchdays); diff --git a/src/index.tsx b/src/index.tsx index 09476fec..6fa3219e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -6,3 +6,7 @@ import App from './App'; const container = document.getElementById('app')!; const root = createRoot(container); root.render(); + +setTimeout(() => { + import('./experiments'); +}, 2000); From 242eac3fc23763e8d2e6928d263c93798d5f206f Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Wed, 26 Jun 2024 23:36:40 +0100 Subject: [PATCH 02/41] bool --- src/experiments/generatePairings.ts | 44 ++++++++--------------------- src/experiments/index.ts | 1 - 2 files changed, 11 insertions(+), 34 deletions(-) diff --git a/src/experiments/generatePairings.ts b/src/experiments/generatePairings.ts index d9a9af40..534b1984 100644 --- a/src/experiments/generatePairings.ts +++ b/src/experiments/generatePairings.ts @@ -8,12 +8,10 @@ const generateMatchdays = ({ teams, numPots, numMatchdays, - maxHomeGamesVsPot, }: { teams: readonly number[]; numPots: number; numMatchdays: number; - maxHomeGamesVsPot: number; }) => { const numTeamsPerPot = teams.length / numPots; const numGamesPerMatchday = teams.length / 2; @@ -58,7 +56,7 @@ const generateMatchdays = ({ */ const hasPlayedWithPotMap: Record< `${number}:${number}:${'h' | 'a'}`, - number + boolean > = {}; while (matches.length < numMatchdays * numGamesPerMatchday) { @@ -99,17 +97,11 @@ const generateMatchdays = ({ const homeTeamPotIndex = Math.floor(m1 / numTeamsPerPot); const awayTeamPotIndex = Math.floor(m2 / numTeamsPerPot); - if ( - c.hasPlayedWithPotMap[`${m1}:${awayTeamPotIndex}:h`] === - maxHomeGamesVsPot - ) { + if (c.hasPlayedWithPotMap[`${m1}:${awayTeamPotIndex}:h`]) { return true; } - if ( - c.hasPlayedWithPotMap[`${m2}:${homeTeamPotIndex}:a`] === - maxHomeGamesVsPot - ) { + if (c.hasPlayedWithPotMap[`${m2}:${homeTeamPotIndex}:a`]) { return true; } @@ -135,14 +127,8 @@ const generateMatchdays = ({ const newHasPlayedWithPotMap: typeof c.hasPlayedWithPotMap = { ...c.hasPlayedWithPotMap, - [`${c.picked[0]}:${pickedAwayPotIndex}:h`]: - (c.hasPlayedWithPotMap[ - `${c.picked[0]}:${pickedAwayPotIndex}:h` - ] ?? 0) + 1, - [`${c.picked[1]}:${pickedHomePotIndex}:a`]: - (c.hasPlayedWithPotMap[ - `${c.picked[1]}:${pickedHomePotIndex}:a` - ] ?? 0) + 1, + [`${c.picked[0]}:${pickedAwayPotIndex}:h`]: true, + [`${c.picked[1]}:${pickedHomePotIndex}:a`]: true, } satisfies typeof c.hasPlayedWithPotMap; const newSource = c.source.filter(m => { @@ -162,17 +148,11 @@ const generateMatchdays = ({ const homePot = Math.floor(m[0] / numTeamsPerPot); const awayPot = Math.floor(m[1] / numTeamsPerPot); - if ( - hasPlayedWithPotMap[`${m[0]}:${awayPot}:h`] === - maxHomeGamesVsPot - ) { + if (hasPlayedWithPotMap[`${m[0]}:${awayPot}:h`]) { return false; } - if ( - hasPlayedWithPotMap[`${m[1]}:${homePot}:a`] === - maxHomeGamesVsPot - ) { + if (hasPlayedWithPotMap[`${m[1]}:${homePot}:a`]) { return false; } @@ -251,10 +231,8 @@ const generateMatchdays = ({ const pickedHomePot = Math.floor(pickedMatch[0] / numTeamsPerPot); const pickedAwayPot = Math.floor(pickedMatch[1] / numTeamsPerPot); - hasPlayedWithPotMap[`${pickedMatch[0]}:${pickedAwayPot}:h`] = - (hasPlayedWithPotMap[`${pickedMatch[0]}:${pickedAwayPot}:h`] ?? 0) + 1; - hasPlayedWithPotMap[`${pickedMatch[1]}:${pickedHomePot}:a`] = - (hasPlayedWithPotMap[`${pickedMatch[1]}:${pickedHomePot}:a`] ?? 0) + 1; + hasPlayedWithPotMap[`${pickedMatch[0]}:${pickedAwayPot}:h`] = true; + hasPlayedWithPotMap[`${pickedMatch[1]}:${pickedHomePot}:a`] = true; console.log(numHomeGamesByTeam, numAwayGamesByTeam); @@ -275,11 +253,11 @@ const generateMatchdays = ({ const aPot = Math.floor(a / numTeamsPerPot); const bPot = Math.floor(b / numTeamsPerPot); - if (hasPlayedWithPotMap[`${a}:${bPot}:h`] === maxHomeGamesVsPot) { + if (hasPlayedWithPotMap[`${a}:${bPot}:h`]) { return false; } - if (hasPlayedWithPotMap[`${b}:${aPot}:a`] === maxHomeGamesVsPot) { + if (hasPlayedWithPotMap[`${b}:${aPot}:a`]) { return false; } diff --git a/src/experiments/index.ts b/src/experiments/index.ts index 1336c3bb..c68dd393 100644 --- a/src/experiments/index.ts +++ b/src/experiments/index.ts @@ -6,6 +6,5 @@ const matchdays = generateMatchdays({ teams: range(36), numPots: 4, numMatchdays: 8, - maxHomeGamesVsPot: 1, }); console.log('final', matchdays); From 452acf0b84e295fef2ffe5282b9e0115e6e47878 Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Thu, 27 Jun 2024 00:10:30 +0100 Subject: [PATCH 03/41] upd --- src/experiments/generatePairings.ts | 442 ++++++++++++++-------------- 1 file changed, 227 insertions(+), 215 deletions(-) diff --git a/src/experiments/generatePairings.ts b/src/experiments/generatePairings.ts index 534b1984..cbfc2955 100644 --- a/src/experiments/generatePairings.ts +++ b/src/experiments/generatePairings.ts @@ -4,6 +4,218 @@ import { findFirstSolution } from '../utils/backtrack'; import generateFull from './generateFull'; +export const getFirstSuitableMatch = ({ + allGames, + matches, + numTeamsPerPot, + numMatchdays, + numGamesPerMatchday, + numPots, +}: { + numPots: number; + allGames: readonly (readonly [number, number])[]; + matches: readonly (readonly [number, number])[]; + numTeamsPerPot: number; + numMatchdays: number; + numGamesPerMatchday: number; +}) => { + const maxGamesAtHome = Math.ceil(numMatchdays / 2); + + const numHomeGamesByTeam: Record = {}; + const numAwayGamesByTeam: Record = {}; + + /** + * team:pot:home? + */ + const hasPlayedWithPotMap: Record< + `${number}:${number}:${'h' | 'a'}`, + boolean + > = {}; + + for (const m of matches) { + const homePot = Math.floor(m[0] / numTeamsPerPot); + const awayPot = Math.floor(m[1] / numTeamsPerPot); + numHomeGamesByTeam[m[0]] = (numHomeGamesByTeam[m[0]] ?? 0) + 1; + numAwayGamesByTeam[m[1]] = (numAwayGamesByTeam[m[1]] ?? 0) + 1; + hasPlayedWithPotMap[`${m[0]}:${awayPot}:h`] = true; + hasPlayedWithPotMap[`${m[1]}:${homePot}:a`] = true; + } + + const remainingGames = allGames.filter(([a, b]) => { + if (numHomeGamesByTeam[a] === maxGamesAtHome) { + return false; + } + if (numAwayGamesByTeam[b] === maxGamesAtHome) { + return false; + } + + const aPot = Math.floor(a / numTeamsPerPot); + const bPot = Math.floor(b / numTeamsPerPot); + + if (hasPlayedWithPotMap[`${a}:${bPot}:h`]) { + return false; + } + + if (hasPlayedWithPotMap[`${b}:${aPot}:a`]) { + return false; + } + + return true; + }); + + return shuffle(remainingGames).find(m => { + console.log('test...', m, { + remainingGames: [...remainingGames], + matches: [...matches], + }); + const solution = findFirstSolution( + { + source: remainingGames, + target: matches, + numHomeGamesByTeam, + numAwayGamesByTeam, + hasPlayedWithPotMap, + picked: m, + }, + { + reject: c => { + const [m1, m2] = c.picked; + + // Ensure the teams play same number of games at home & away + if (c.numHomeGamesByTeam[m1] === maxGamesAtHome) { + return true; + } + if (c.numAwayGamesByTeam[m2] === maxGamesAtHome) { + return true; + } + + const homeTeamPotIndex = Math.floor(m1 / numTeamsPerPot); + const awayTeamPotIndex = Math.floor(m2 / numTeamsPerPot); + + if (c.hasPlayedWithPotMap[`${m1}:${awayTeamPotIndex}:h`]) { + return true; + } + + if (c.hasPlayedWithPotMap[`${m2}:${homeTeamPotIndex}:a`]) { + return true; + } + + return false; + }, + + accept: c => c.target.length === numMatchdays * numGamesPerMatchday - 1, + + generate: c => { + const newTarget = [...c.target, c.picked]; + const newNumHomeGamesByTeam = { + ...c.numHomeGamesByTeam, + [c.picked[0]]: (c.numHomeGamesByTeam[c.picked[0]] ?? 0) + 1, + } as typeof c.numHomeGamesByTeam; + const newNumAwayGamesByTeam = { + ...c.numAwayGamesByTeam, + [c.picked[1]]: (c.numAwayGamesByTeam[c.picked[1]] ?? 0) + 1, + } as typeof c.numAwayGamesByTeam; + + const pickedHomePotIndex = Math.floor(c.picked[0] / numTeamsPerPot); + const pickedAwayPotIndex = Math.floor(c.picked[1] / numTeamsPerPot); + + const newHasPlayedWithPotMap: typeof c.hasPlayedWithPotMap = { + ...c.hasPlayedWithPotMap, + [`${c.picked[0]}:${pickedAwayPotIndex}:h`]: true, + [`${c.picked[1]}:${pickedHomePotIndex}:a`]: true, + } satisfies typeof c.hasPlayedWithPotMap; + + const newSource = c.source.filter(([h, a]) => { + if ( + (h === c.picked[0] && a === c.picked[1]) || + (h === c.picked[1] && a === c.picked[0]) + ) { + return false; + } + if (newNumHomeGamesByTeam[h] === maxGamesAtHome) { + return false; + } + if (newNumAwayGamesByTeam[a] === maxGamesAtHome) { + return false; + } + + const homePot = Math.floor(h / numTeamsPerPot); + const awayPot = Math.floor(a / numTeamsPerPot); + + if (hasPlayedWithPotMap[`${h}:${awayPot}:h`]) { + return false; + } + + if (hasPlayedWithPotMap[`${a}:${homePot}:a`]) { + return false; + } + + return true; + }); + + const candidates: (typeof c)[] = []; + + const lowestRemainingTeam = + newSource.length > 0 + ? // eslint-disable-next-line unicorn/no-array-reduce + newSource.reduce( + (prev, cur) => Math.min(prev, ...cur), + Math.min(...newSource[0]), + ) + : undefined; + + if (lowestRemainingTeam !== undefined) { + let nextPot: number | undefined; + let nextPlace: 'h' | 'a' | undefined; + for (let i = 0; i < numPots; ++i) { + if (!newHasPlayedWithPotMap[`${lowestRemainingTeam}:${i}:h`]) { + nextPot = i; + nextPlace = 'h'; + break; + } + if (!newHasPlayedWithPotMap[`${lowestRemainingTeam}:${i}:a`]) { + nextPot = i; + nextPlace = 'a'; + break; + } + } + + for (const newPicked of newSource) { + const homePot = Math.floor(newPicked[0] / numTeamsPerPot); + const awayPot = Math.floor(newPicked[1] / numTeamsPerPot); + + const isMatchGood = + (nextPlace === 'h' && + newPicked[0] === lowestRemainingTeam && + awayPot === nextPot) || + (nextPlace === 'a' && + newPicked[1] === lowestRemainingTeam && + homePot === nextPot); + if (isMatchGood) { + candidates.push({ + source: newSource, + target: newTarget, + picked: newPicked, + numHomeGamesByTeam: newNumHomeGamesByTeam, + numAwayGamesByTeam: newNumAwayGamesByTeam, + hasPlayedWithPotMap: newHasPlayedWithPotMap, + }); + } + } + } + + return candidates; + }, + }, + ); + if (!solution) { + console.log('sol', solution); + } + // console.log('sol', solution); + return solution !== undefined; + })!; +}; + const generateMatchdays = ({ teams, numPots, @@ -16,18 +228,16 @@ const generateMatchdays = ({ const numTeamsPerPot = teams.length / numPots; const numGamesPerMatchday = teams.length / 2; - const maxGamesAtHome = Math.ceil(numMatchdays / 2); - console.log('doing for', numMatchdays); - let remainingGames = generateFull(teams); + let allGames = generateFull(teams); - remainingGames = [ - ...remainingGames, - ...remainingGames.map(([a, b]) => [b, a] as [number, number]), + allGames = [ + ...allGames, + ...allGames.map(([a, b]) => [b, a] as [number, number]), ]; - remainingGames = orderBy(remainingGames, [ + allGames = orderBy(allGames, [ m => Math.min(...m), m => Math.max(...m), // () => Math.random(), @@ -45,225 +255,27 @@ const generateMatchdays = ({ // remainingGames = shuffle(remainingGames); - console.log('initial games', JSON.stringify(remainingGames)); + console.log('initial games', JSON.stringify(allGames)); - const matches: [number, number][] = []; - const numHomeGamesByTeam: Record = {}; - const numAwayGamesByTeam: Record = {}; - - /** - * team:pot:home? - */ - const hasPlayedWithPotMap: Record< - `${number}:${number}:${'h' | 'a'}`, - boolean - > = {}; + const matches: (readonly [number, number])[] = []; while (matches.length < numMatchdays * numGamesPerMatchday) { console.log('nice'); // remainingGames = shuffle(remainingGames); - console.log(numHomeGamesByTeam, numAwayGamesByTeam, hasPlayedWithPotMap); - - console.log('orderedRemainingGames', [...remainingGames]); - - // eslint-disable-next-line no-loop-func - const pickedMatch = shuffle(remainingGames).find(m => { - console.log('test...', m, { - remainingGames: [...remainingGames], - matches: [...matches], - }); - const solution = findFirstSolution( - { - source: remainingGames, - target: matches, - numHomeGamesByTeam, - numAwayGamesByTeam, - hasPlayedWithPotMap, - picked: m, - }, - { - reject: c => { - const [m1, m2] = c.picked; - - // Ensure the teams play same number of games at home & away - if (c.numHomeGamesByTeam[m1] === maxGamesAtHome) { - return true; - } - if (c.numAwayGamesByTeam[m2] === maxGamesAtHome) { - return true; - } - - const homeTeamPotIndex = Math.floor(m1 / numTeamsPerPot); - const awayTeamPotIndex = Math.floor(m2 / numTeamsPerPot); - - if (c.hasPlayedWithPotMap[`${m1}:${awayTeamPotIndex}:h`]) { - return true; - } - - if (c.hasPlayedWithPotMap[`${m2}:${homeTeamPotIndex}:a`]) { - return true; - } - - return false; - }, - - accept: c => - c.target.length === numMatchdays * numGamesPerMatchday - 1, - - generate: c => { - const newTarget = [...c.target, c.picked]; - const newNumHomeGamesByTeam = { - ...c.numHomeGamesByTeam, - [c.picked[0]]: (c.numHomeGamesByTeam[c.picked[0]] ?? 0) + 1, - } as typeof c.numHomeGamesByTeam; - const newNumAwayGamesByTeam = { - ...c.numAwayGamesByTeam, - [c.picked[1]]: (c.numAwayGamesByTeam[c.picked[1]] ?? 0) + 1, - } as typeof c.numAwayGamesByTeam; - - const pickedHomePotIndex = Math.floor(c.picked[0] / numTeamsPerPot); - const pickedAwayPotIndex = Math.floor(c.picked[1] / numTeamsPerPot); - - const newHasPlayedWithPotMap: typeof c.hasPlayedWithPotMap = { - ...c.hasPlayedWithPotMap, - [`${c.picked[0]}:${pickedAwayPotIndex}:h`]: true, - [`${c.picked[1]}:${pickedHomePotIndex}:a`]: true, - } satisfies typeof c.hasPlayedWithPotMap; - - const newSource = c.source.filter(m => { - if ( - (m[0] === c.picked[0] && m[1] === c.picked[1]) || - (m[0] === c.picked[1] && m[1] === c.picked[0]) - ) { - return false; - } - if (newNumHomeGamesByTeam[m[0]] === maxGamesAtHome) { - return false; - } - if (newNumAwayGamesByTeam[m[1]] === maxGamesAtHome) { - return false; - } - - const homePot = Math.floor(m[0] / numTeamsPerPot); - const awayPot = Math.floor(m[1] / numTeamsPerPot); - - if (hasPlayedWithPotMap[`${m[0]}:${awayPot}:h`]) { - return false; - } - - if (hasPlayedWithPotMap[`${m[1]}:${homePot}:a`]) { - return false; - } - - return true; - }); - - const candidates: (typeof c)[] = []; - - const lowestRemainingTeam = - newSource.length > 0 - ? // eslint-disable-next-line unicorn/no-array-reduce - newSource.reduce( - (prev, cur) => Math.min(prev, ...cur), - Math.min(...newSource[0]), - ) - : undefined; - - if (lowestRemainingTeam !== undefined) { - let nextPot: number | undefined; - let nextPlace: 'h' | 'a' | undefined; - for (let i = 0; i < numPots; ++i) { - if (!newHasPlayedWithPotMap[`${lowestRemainingTeam}:${i}:h`]) { - nextPot = i; - nextPlace = 'h'; - break; - } - if (!newHasPlayedWithPotMap[`${lowestRemainingTeam}:${i}:a`]) { - nextPot = i; - nextPlace = 'a'; - break; - } - } - - for (const newPicked of newSource) { - const homePot = Math.floor(newPicked[0] / numTeamsPerPot); - const awayPot = Math.floor(newPicked[1] / numTeamsPerPot); - - const isMatchGood = - (nextPlace === 'h' && - newPicked[0] === lowestRemainingTeam && - awayPot === nextPot) || - (nextPlace === 'a' && - newPicked[1] === lowestRemainingTeam && - homePot === nextPot); - if (isMatchGood) { - candidates.push({ - source: newSource, - target: newTarget, - picked: newPicked, - numHomeGamesByTeam: newNumHomeGamesByTeam, - numAwayGamesByTeam: newNumAwayGamesByTeam, - hasPlayedWithPotMap: newHasPlayedWithPotMap, - }); - } - } - } + const pickedMatch = getFirstSuitableMatch({ + allGames, + matches, + numTeamsPerPot, + numMatchdays, + numGamesPerMatchday, + numPots, + }); - return candidates; - }, - }, - ); - if (!solution) { - console.log('sol', solution); - } - // console.log('sol', solution); - return solution !== undefined; - })!; console.log('taking', pickedMatch); matches.push(pickedMatch); - numHomeGamesByTeam[pickedMatch[0]] = - (numHomeGamesByTeam[pickedMatch[0]] ?? 0) + 1; - numAwayGamesByTeam[pickedMatch[1]] = - (numAwayGamesByTeam[pickedMatch[1]] ?? 0) + 1; - - const pickedHomePot = Math.floor(pickedMatch[0] / numTeamsPerPot); - const pickedAwayPot = Math.floor(pickedMatch[1] / numTeamsPerPot); - hasPlayedWithPotMap[`${pickedMatch[0]}:${pickedAwayPot}:h`] = true; - hasPlayedWithPotMap[`${pickedMatch[1]}:${pickedHomePot}:a`] = true; - - console.log(numHomeGamesByTeam, numAwayGamesByTeam); - - remainingGames = remainingGames.filter(([a, b]) => { - const justPicked = - (a === pickedMatch[0] && b === pickedMatch[1]) || - (a === pickedMatch[1] && b === pickedMatch[0]); - if (justPicked) { - return false; - } - if (numHomeGamesByTeam[a] === maxGamesAtHome) { - return false; - } - if (numAwayGamesByTeam[b] === maxGamesAtHome) { - return false; - } - - const aPot = Math.floor(a / numTeamsPerPot); - const bPot = Math.floor(b / numTeamsPerPot); - - if (hasPlayedWithPotMap[`${a}:${bPot}:h`]) { - return false; - } - - if (hasPlayedWithPotMap[`${b}:${aPot}:a`]) { - return false; - } - - return true; - }); - console.log('new remaining', remainingGames); console.log('current total', matches.length); } From d2ce6fb564fb6ef342d7d1a629b617756c402f8d Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Thu, 27 Jun 2024 00:30:20 +0100 Subject: [PATCH 04/41] upd --- src/experiments/generatePairings.ts | 6 ++++-- src/experiments/index.ts | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/experiments/generatePairings.ts b/src/experiments/generatePairings.ts index cbfc2955..70738781 100644 --- a/src/experiments/generatePairings.ts +++ b/src/experiments/generatePairings.ts @@ -220,16 +220,16 @@ const generateMatchdays = ({ teams, numPots, numMatchdays, + canPlay, }: { teams: readonly number[]; numPots: number; numMatchdays: number; + canPlay: (a: number, b: number) => boolean; }) => { const numTeamsPerPot = teams.length / numPots; const numGamesPerMatchday = teams.length / 2; - console.log('doing for', numMatchdays); - let allGames = generateFull(teams); allGames = [ @@ -253,6 +253,8 @@ const generateMatchdays = ({ // ([a, b]) => -Math.abs(a - b), ]); + allGames = allGames.filter(([a, b]) => canPlay(a, b)); + // remainingGames = shuffle(remainingGames); console.log('initial games', JSON.stringify(allGames)); diff --git a/src/experiments/index.ts b/src/experiments/index.ts index c68dd393..c47fda97 100644 --- a/src/experiments/index.ts +++ b/src/experiments/index.ts @@ -1,4 +1,4 @@ -import { range } from 'lodash'; +import { range, stubTrue } from 'lodash'; import generateMatchdays from './generatePairings'; @@ -6,5 +6,6 @@ const matchdays = generateMatchdays({ teams: range(36), numPots: 4, numMatchdays: 8, + canPlay: stubTrue, }); console.log('final', matchdays); From a8e1c016ffc1fbdedc53e09eca9466a4096c5ab9 Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Thu, 27 Jun 2024 00:32:57 +0100 Subject: [PATCH 05/41] upd --- src/experiments/generatePairings.ts | 217 +---------------------- src/experiments/getFirstSuitableMatch.ts | 215 ++++++++++++++++++++++ 2 files changed, 217 insertions(+), 215 deletions(-) create mode 100644 src/experiments/getFirstSuitableMatch.ts diff --git a/src/experiments/generatePairings.ts b/src/experiments/generatePairings.ts index 70738781..0c363f50 100644 --- a/src/experiments/generatePairings.ts +++ b/src/experiments/generatePairings.ts @@ -1,220 +1,7 @@ -import { orderBy, shuffle } from 'lodash'; - -import { findFirstSolution } from '../utils/backtrack'; +import { orderBy } from 'lodash'; import generateFull from './generateFull'; - -export const getFirstSuitableMatch = ({ - allGames, - matches, - numTeamsPerPot, - numMatchdays, - numGamesPerMatchday, - numPots, -}: { - numPots: number; - allGames: readonly (readonly [number, number])[]; - matches: readonly (readonly [number, number])[]; - numTeamsPerPot: number; - numMatchdays: number; - numGamesPerMatchday: number; -}) => { - const maxGamesAtHome = Math.ceil(numMatchdays / 2); - - const numHomeGamesByTeam: Record = {}; - const numAwayGamesByTeam: Record = {}; - - /** - * team:pot:home? - */ - const hasPlayedWithPotMap: Record< - `${number}:${number}:${'h' | 'a'}`, - boolean - > = {}; - - for (const m of matches) { - const homePot = Math.floor(m[0] / numTeamsPerPot); - const awayPot = Math.floor(m[1] / numTeamsPerPot); - numHomeGamesByTeam[m[0]] = (numHomeGamesByTeam[m[0]] ?? 0) + 1; - numAwayGamesByTeam[m[1]] = (numAwayGamesByTeam[m[1]] ?? 0) + 1; - hasPlayedWithPotMap[`${m[0]}:${awayPot}:h`] = true; - hasPlayedWithPotMap[`${m[1]}:${homePot}:a`] = true; - } - - const remainingGames = allGames.filter(([a, b]) => { - if (numHomeGamesByTeam[a] === maxGamesAtHome) { - return false; - } - if (numAwayGamesByTeam[b] === maxGamesAtHome) { - return false; - } - - const aPot = Math.floor(a / numTeamsPerPot); - const bPot = Math.floor(b / numTeamsPerPot); - - if (hasPlayedWithPotMap[`${a}:${bPot}:h`]) { - return false; - } - - if (hasPlayedWithPotMap[`${b}:${aPot}:a`]) { - return false; - } - - return true; - }); - - return shuffle(remainingGames).find(m => { - console.log('test...', m, { - remainingGames: [...remainingGames], - matches: [...matches], - }); - const solution = findFirstSolution( - { - source: remainingGames, - target: matches, - numHomeGamesByTeam, - numAwayGamesByTeam, - hasPlayedWithPotMap, - picked: m, - }, - { - reject: c => { - const [m1, m2] = c.picked; - - // Ensure the teams play same number of games at home & away - if (c.numHomeGamesByTeam[m1] === maxGamesAtHome) { - return true; - } - if (c.numAwayGamesByTeam[m2] === maxGamesAtHome) { - return true; - } - - const homeTeamPotIndex = Math.floor(m1 / numTeamsPerPot); - const awayTeamPotIndex = Math.floor(m2 / numTeamsPerPot); - - if (c.hasPlayedWithPotMap[`${m1}:${awayTeamPotIndex}:h`]) { - return true; - } - - if (c.hasPlayedWithPotMap[`${m2}:${homeTeamPotIndex}:a`]) { - return true; - } - - return false; - }, - - accept: c => c.target.length === numMatchdays * numGamesPerMatchday - 1, - - generate: c => { - const newTarget = [...c.target, c.picked]; - const newNumHomeGamesByTeam = { - ...c.numHomeGamesByTeam, - [c.picked[0]]: (c.numHomeGamesByTeam[c.picked[0]] ?? 0) + 1, - } as typeof c.numHomeGamesByTeam; - const newNumAwayGamesByTeam = { - ...c.numAwayGamesByTeam, - [c.picked[1]]: (c.numAwayGamesByTeam[c.picked[1]] ?? 0) + 1, - } as typeof c.numAwayGamesByTeam; - - const pickedHomePotIndex = Math.floor(c.picked[0] / numTeamsPerPot); - const pickedAwayPotIndex = Math.floor(c.picked[1] / numTeamsPerPot); - - const newHasPlayedWithPotMap: typeof c.hasPlayedWithPotMap = { - ...c.hasPlayedWithPotMap, - [`${c.picked[0]}:${pickedAwayPotIndex}:h`]: true, - [`${c.picked[1]}:${pickedHomePotIndex}:a`]: true, - } satisfies typeof c.hasPlayedWithPotMap; - - const newSource = c.source.filter(([h, a]) => { - if ( - (h === c.picked[0] && a === c.picked[1]) || - (h === c.picked[1] && a === c.picked[0]) - ) { - return false; - } - if (newNumHomeGamesByTeam[h] === maxGamesAtHome) { - return false; - } - if (newNumAwayGamesByTeam[a] === maxGamesAtHome) { - return false; - } - - const homePot = Math.floor(h / numTeamsPerPot); - const awayPot = Math.floor(a / numTeamsPerPot); - - if (hasPlayedWithPotMap[`${h}:${awayPot}:h`]) { - return false; - } - - if (hasPlayedWithPotMap[`${a}:${homePot}:a`]) { - return false; - } - - return true; - }); - - const candidates: (typeof c)[] = []; - - const lowestRemainingTeam = - newSource.length > 0 - ? // eslint-disable-next-line unicorn/no-array-reduce - newSource.reduce( - (prev, cur) => Math.min(prev, ...cur), - Math.min(...newSource[0]), - ) - : undefined; - - if (lowestRemainingTeam !== undefined) { - let nextPot: number | undefined; - let nextPlace: 'h' | 'a' | undefined; - for (let i = 0; i < numPots; ++i) { - if (!newHasPlayedWithPotMap[`${lowestRemainingTeam}:${i}:h`]) { - nextPot = i; - nextPlace = 'h'; - break; - } - if (!newHasPlayedWithPotMap[`${lowestRemainingTeam}:${i}:a`]) { - nextPot = i; - nextPlace = 'a'; - break; - } - } - - for (const newPicked of newSource) { - const homePot = Math.floor(newPicked[0] / numTeamsPerPot); - const awayPot = Math.floor(newPicked[1] / numTeamsPerPot); - - const isMatchGood = - (nextPlace === 'h' && - newPicked[0] === lowestRemainingTeam && - awayPot === nextPot) || - (nextPlace === 'a' && - newPicked[1] === lowestRemainingTeam && - homePot === nextPot); - if (isMatchGood) { - candidates.push({ - source: newSource, - target: newTarget, - picked: newPicked, - numHomeGamesByTeam: newNumHomeGamesByTeam, - numAwayGamesByTeam: newNumAwayGamesByTeam, - hasPlayedWithPotMap: newHasPlayedWithPotMap, - }); - } - } - } - - return candidates; - }, - }, - ); - if (!solution) { - console.log('sol', solution); - } - // console.log('sol', solution); - return solution !== undefined; - })!; -}; +import getFirstSuitableMatch from './getFirstSuitableMatch'; const generateMatchdays = ({ teams, diff --git a/src/experiments/getFirstSuitableMatch.ts b/src/experiments/getFirstSuitableMatch.ts new file mode 100644 index 00000000..34b80c96 --- /dev/null +++ b/src/experiments/getFirstSuitableMatch.ts @@ -0,0 +1,215 @@ +import { shuffle } from 'lodash'; + +import { findFirstSolution } from '../utils/backtrack'; + +export default ({ + allGames, + matches, + numTeamsPerPot, + numMatchdays, + numGamesPerMatchday, + numPots, +}: { + numPots: number; + allGames: readonly (readonly [number, number])[]; + matches: readonly (readonly [number, number])[]; + numTeamsPerPot: number; + numMatchdays: number; + numGamesPerMatchday: number; +}) => { + const maxGamesAtHome = Math.ceil(numMatchdays / 2); + + const numHomeGamesByTeam: Record = {}; + const numAwayGamesByTeam: Record = {}; + + /** + * team:pot:home? + */ + const hasPlayedWithPotMap: Record< + `${number}:${number}:${'h' | 'a'}`, + boolean + > = {}; + + for (const m of matches) { + const homePot = Math.floor(m[0] / numTeamsPerPot); + const awayPot = Math.floor(m[1] / numTeamsPerPot); + numHomeGamesByTeam[m[0]] = (numHomeGamesByTeam[m[0]] ?? 0) + 1; + numAwayGamesByTeam[m[1]] = (numAwayGamesByTeam[m[1]] ?? 0) + 1; + hasPlayedWithPotMap[`${m[0]}:${awayPot}:h`] = true; + hasPlayedWithPotMap[`${m[1]}:${homePot}:a`] = true; + } + + const remainingGames = allGames.filter(([a, b]) => { + if (numHomeGamesByTeam[a] === maxGamesAtHome) { + return false; + } + if (numAwayGamesByTeam[b] === maxGamesAtHome) { + return false; + } + + const aPot = Math.floor(a / numTeamsPerPot); + const bPot = Math.floor(b / numTeamsPerPot); + + if (hasPlayedWithPotMap[`${a}:${bPot}:h`]) { + return false; + } + + if (hasPlayedWithPotMap[`${b}:${aPot}:a`]) { + return false; + } + + return true; + }); + + return shuffle(remainingGames).find(m => { + console.log('test...', m, { + remainingGames: [...remainingGames], + matches: [...matches], + }); + const solution = findFirstSolution( + { + source: remainingGames, + target: matches, + numHomeGamesByTeam, + numAwayGamesByTeam, + hasPlayedWithPotMap, + picked: m, + }, + { + reject: c => { + const [m1, m2] = c.picked; + + // Ensure the teams play same number of games at home & away + if (c.numHomeGamesByTeam[m1] === maxGamesAtHome) { + return true; + } + if (c.numAwayGamesByTeam[m2] === maxGamesAtHome) { + return true; + } + + const homeTeamPotIndex = Math.floor(m1 / numTeamsPerPot); + const awayTeamPotIndex = Math.floor(m2 / numTeamsPerPot); + + if (c.hasPlayedWithPotMap[`${m1}:${awayTeamPotIndex}:h`]) { + return true; + } + + if (c.hasPlayedWithPotMap[`${m2}:${homeTeamPotIndex}:a`]) { + return true; + } + + return false; + }, + + accept: c => c.target.length === numMatchdays * numGamesPerMatchday - 1, + + generate: c => { + const newTarget = [...c.target, c.picked]; + const newNumHomeGamesByTeam = { + ...c.numHomeGamesByTeam, + [c.picked[0]]: (c.numHomeGamesByTeam[c.picked[0]] ?? 0) + 1, + } as typeof c.numHomeGamesByTeam; + const newNumAwayGamesByTeam = { + ...c.numAwayGamesByTeam, + [c.picked[1]]: (c.numAwayGamesByTeam[c.picked[1]] ?? 0) + 1, + } as typeof c.numAwayGamesByTeam; + + const pickedHomePotIndex = Math.floor(c.picked[0] / numTeamsPerPot); + const pickedAwayPotIndex = Math.floor(c.picked[1] / numTeamsPerPot); + + const newHasPlayedWithPotMap: typeof c.hasPlayedWithPotMap = { + ...c.hasPlayedWithPotMap, + [`${c.picked[0]}:${pickedAwayPotIndex}:h`]: true, + [`${c.picked[1]}:${pickedHomePotIndex}:a`]: true, + } satisfies typeof c.hasPlayedWithPotMap; + + const newSource = c.source.filter(([h, a]) => { + if ( + (h === c.picked[0] && a === c.picked[1]) || + (h === c.picked[1] && a === c.picked[0]) + ) { + return false; + } + if (newNumHomeGamesByTeam[h] === maxGamesAtHome) { + return false; + } + if (newNumAwayGamesByTeam[a] === maxGamesAtHome) { + return false; + } + + const homePot = Math.floor(h / numTeamsPerPot); + const awayPot = Math.floor(a / numTeamsPerPot); + + if (hasPlayedWithPotMap[`${h}:${awayPot}:h`]) { + return false; + } + + if (hasPlayedWithPotMap[`${a}:${homePot}:a`]) { + return false; + } + + return true; + }); + + const candidates: (typeof c)[] = []; + + const lowestRemainingTeam = + newSource.length > 0 + ? // eslint-disable-next-line unicorn/no-array-reduce + newSource.reduce( + (prev, cur) => Math.min(prev, ...cur), + Math.min(...newSource[0]), + ) + : undefined; + + if (lowestRemainingTeam !== undefined) { + let nextPot: number | undefined; + let nextPlace: 'h' | 'a' | undefined; + for (let i = 0; i < numPots; ++i) { + if (!newHasPlayedWithPotMap[`${lowestRemainingTeam}:${i}:h`]) { + nextPot = i; + nextPlace = 'h'; + break; + } + if (!newHasPlayedWithPotMap[`${lowestRemainingTeam}:${i}:a`]) { + nextPot = i; + nextPlace = 'a'; + break; + } + } + + for (const newPicked of newSource) { + const homePot = Math.floor(newPicked[0] / numTeamsPerPot); + const awayPot = Math.floor(newPicked[1] / numTeamsPerPot); + + const isMatchGood = + (nextPlace === 'h' && + newPicked[0] === lowestRemainingTeam && + awayPot === nextPot) || + (nextPlace === 'a' && + newPicked[1] === lowestRemainingTeam && + homePot === nextPot); + if (isMatchGood) { + candidates.push({ + source: newSource, + target: newTarget, + picked: newPicked, + numHomeGamesByTeam: newNumHomeGamesByTeam, + numAwayGamesByTeam: newNumAwayGamesByTeam, + hasPlayedWithPotMap: newHasPlayedWithPotMap, + }); + } + } + } + + return candidates; + }, + }, + ); + if (!solution) { + console.log('sol', solution); + } + // console.log('sol', solution); + return solution !== undefined; + })!; +}; From 0d77c08a3529ea4292c06a5113996a17192433db Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Thu, 27 Jun 2024 00:33:24 +0100 Subject: [PATCH 06/41] upd --- src/experiments/generatePairings.ts | 4 +--- src/experiments/index.ts | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/experiments/generatePairings.ts b/src/experiments/generatePairings.ts index 0c363f50..e7566fb4 100644 --- a/src/experiments/generatePairings.ts +++ b/src/experiments/generatePairings.ts @@ -3,7 +3,7 @@ import { orderBy } from 'lodash'; import generateFull from './generateFull'; import getFirstSuitableMatch from './getFirstSuitableMatch'; -const generateMatchdays = ({ +export default ({ teams, numPots, numMatchdays, @@ -72,5 +72,3 @@ const generateMatchdays = ({ return matches; }; - -export default generateMatchdays; diff --git a/src/experiments/index.ts b/src/experiments/index.ts index c47fda97..01b702b7 100644 --- a/src/experiments/index.ts +++ b/src/experiments/index.ts @@ -1,8 +1,8 @@ import { range, stubTrue } from 'lodash'; -import generateMatchdays from './generatePairings'; +import generatePairings from './generatePairings'; -const matchdays = generateMatchdays({ +const matchdays = generatePairings({ teams: range(36), numPots: 4, numMatchdays: 8, From 1387dac74c5aaff7c96396c90ae44d013954f50c Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Thu, 27 Jun 2024 00:35:59 +0100 Subject: [PATCH 07/41] upd --- src/experiments/generatePairings.ts | 6 +++--- src/experiments/getFirstSuitableMatch.ts | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/experiments/generatePairings.ts b/src/experiments/generatePairings.ts index e7566fb4..57cb9417 100644 --- a/src/experiments/generatePairings.ts +++ b/src/experiments/generatePairings.ts @@ -53,12 +53,12 @@ export default ({ // remainingGames = shuffle(remainingGames); const pickedMatch = getFirstSuitableMatch({ - allGames, - matches, + numPots, numTeamsPerPot, numMatchdays, numGamesPerMatchday, - numPots, + allGames, + pickedMatches: matches, }); console.log('taking', pickedMatch); diff --git a/src/experiments/getFirstSuitableMatch.ts b/src/experiments/getFirstSuitableMatch.ts index 34b80c96..110cbe49 100644 --- a/src/experiments/getFirstSuitableMatch.ts +++ b/src/experiments/getFirstSuitableMatch.ts @@ -3,19 +3,19 @@ import { shuffle } from 'lodash'; import { findFirstSolution } from '../utils/backtrack'; export default ({ - allGames, - matches, + numPots, numTeamsPerPot, numMatchdays, numGamesPerMatchday, - numPots, + allGames, + pickedMatches, }: { numPots: number; - allGames: readonly (readonly [number, number])[]; - matches: readonly (readonly [number, number])[]; numTeamsPerPot: number; numMatchdays: number; numGamesPerMatchday: number; + allGames: readonly (readonly [number, number])[]; + pickedMatches: readonly (readonly [number, number])[]; }) => { const maxGamesAtHome = Math.ceil(numMatchdays / 2); @@ -30,7 +30,7 @@ export default ({ boolean > = {}; - for (const m of matches) { + for (const m of pickedMatches) { const homePot = Math.floor(m[0] / numTeamsPerPot); const awayPot = Math.floor(m[1] / numTeamsPerPot); numHomeGamesByTeam[m[0]] = (numHomeGamesByTeam[m[0]] ?? 0) + 1; @@ -64,12 +64,12 @@ export default ({ return shuffle(remainingGames).find(m => { console.log('test...', m, { remainingGames: [...remainingGames], - matches: [...matches], + pickedMatches: [...pickedMatches], }); const solution = findFirstSolution( { source: remainingGames, - target: matches, + target: pickedMatches, numHomeGamesByTeam, numAwayGamesByTeam, hasPlayedWithPotMap, From 9ca1a8991f37de4d873897f30349991b44b76c97 Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Thu, 27 Jun 2024 01:37:49 +0100 Subject: [PATCH 08/41] upd --- src/experiments/generateFull.ts | 4 +- src/experiments/generatePairings.ts | 37 +++--- src/experiments/getFirstSuitableMatch.ts | 16 ++- src/experiments/index.ts | 23 +++- src/experiments/pots.ts | 157 +++++++++++++++++++++++ 5 files changed, 206 insertions(+), 31 deletions(-) create mode 100644 src/experiments/pots.ts diff --git a/src/experiments/generateFull.ts b/src/experiments/generateFull.ts index 7b537a89..7830a397 100644 --- a/src/experiments/generateFull.ts +++ b/src/experiments/generateFull.ts @@ -1,9 +1,9 @@ export default (teams: readonly T[], numTimes = 1) => { - const matches: [T, T][] = []; + const matches: (readonly [T, T])[] = []; for (let k = 0; k < numTimes; ++k) { for (let i = 0; i < teams.length - 1; ++i) { for (let j = i + 1; j < teams.length; ++j) { - const match: [T, T] = + const match = k & 1 ? ([teams[j], teams[i]] as const) : ([teams[i], teams[j]] as const); diff --git a/src/experiments/generatePairings.ts b/src/experiments/generatePairings.ts index 57cb9417..1ae26577 100644 --- a/src/experiments/generatePairings.ts +++ b/src/experiments/generatePairings.ts @@ -1,28 +1,27 @@ -import { orderBy } from 'lodash'; +import { orderBy, range, shuffle } from 'lodash'; import generateFull from './generateFull'; import getFirstSuitableMatch from './getFirstSuitableMatch'; -export default ({ - teams, - numPots, +export default ({ + pots, numMatchdays, - canPlay, + isMatchPossible, }: { - teams: readonly number[]; - numPots: number; + pots: readonly (readonly T[])[]; numMatchdays: number; - canPlay: (a: number, b: number) => boolean; + isMatchPossible: (h: T, a: T) => boolean; }) => { - const numTeamsPerPot = teams.length / numPots; + console.log(JSON.stringify(pots)); + const teams = pots.flat(); + const numTeamsPerPot = pots[0].length; const numGamesPerMatchday = teams.length / 2; - let allGames = generateFull(teams); + const teamIndices = range(teams.length); - allGames = [ - ...allGames, - ...allGames.map(([a, b]) => [b, a] as [number, number]), - ]; + let allGames = generateFull(teamIndices); + + allGames = [...allGames, ...allGames.map(([a, b]) => [b, a] as const)]; allGames = orderBy(allGames, [ m => Math.min(...m), @@ -40,11 +39,11 @@ export default ({ // ([a, b]) => -Math.abs(a - b), ]); - allGames = allGames.filter(([a, b]) => canPlay(a, b)); + allGames = allGames.filter(([h, a]) => isMatchPossible(teams[h], teams[a])); - // remainingGames = shuffle(remainingGames); + allGames = shuffle(allGames); - console.log('initial games', JSON.stringify(allGames)); + console.log('initial games', allGames.length, JSON.stringify(allGames)); const matches: (readonly [number, number])[] = []; @@ -53,7 +52,7 @@ export default ({ // remainingGames = shuffle(remainingGames); const pickedMatch = getFirstSuitableMatch({ - numPots, + numPots: pots.length, numTeamsPerPot, numMatchdays, numGamesPerMatchday, @@ -70,5 +69,5 @@ export default ({ console.log('done for num matchdays:', numMatchdays); - return matches; + return matches.map(([h, a]) => [teams[h], teams[a]] as const); }; diff --git a/src/experiments/getFirstSuitableMatch.ts b/src/experiments/getFirstSuitableMatch.ts index 110cbe49..424d5af1 100644 --- a/src/experiments/getFirstSuitableMatch.ts +++ b/src/experiments/getFirstSuitableMatch.ts @@ -40,6 +40,15 @@ export default ({ } const remainingGames = allGames.filter(([a, b]) => { + if ( + pickedMatches.some( + m => (m[0] === a && m[1] === b) || (m[0] === b && m[1] === a), + ) + ) { + // already played before + return false; + } + if (numHomeGamesByTeam[a] === maxGamesAtHome) { return false; } @@ -61,11 +70,10 @@ export default ({ return true; }); + console.log('num remaining possible games', remainingGames.length); + return shuffle(remainingGames).find(m => { - console.log('test...', m, { - remainingGames: [...remainingGames], - pickedMatches: [...pickedMatches], - }); + console.log('test...', m); const solution = findFirstSolution( { source: remainingGames, diff --git a/src/experiments/index.ts b/src/experiments/index.ts index 01b702b7..af1816f4 100644 --- a/src/experiments/index.ts +++ b/src/experiments/index.ts @@ -1,11 +1,22 @@ -import { range, stubTrue } from 'lodash'; +import { chunk, range, stubTrue } from 'lodash'; import generatePairings from './generatePairings'; +import pots from './pots'; + +const NUM_MATCHDAYS = 8; + +type Team = (typeof pots)[number][number]; + +function canPlay(a: Team, b: Team) { + return a.country !== b.country; +} const matchdays = generatePairings({ - teams: range(36), - numPots: 4, - numMatchdays: 8, - canPlay: stubTrue, + pots, + numMatchdays: NUM_MATCHDAYS, + isMatchPossible: canPlay, }); -console.log('final', matchdays); +console.log( + 'final', + matchdays.map(m => [m[0].name, m[1].name]), +); diff --git a/src/experiments/pots.ts b/src/experiments/pots.ts new file mode 100644 index 00000000..504da16d --- /dev/null +++ b/src/experiments/pots.ts @@ -0,0 +1,157 @@ +export default [ + [ + { + name: 'Real Madrid', + country: 'Spain', + }, + { + name: 'Man City', + country: 'England', + }, + { + name: 'Bayern', + country: 'Germany', + }, + { + name: 'Paris', + country: 'France', + }, + { + name: 'Liverpool', + country: 'England', + }, + { + name: 'Internazionale', + country: 'Italy', + }, + { + name: 'Dortmund', + country: 'Germany', + }, + { + name: 'Leipzig', + country: 'Germany', + }, + { + name: 'Barcelona', + country: 'Spain', + }, + ], + [ + { + name: 'Leverkusen', + country: 'Germany', + }, + { + name: 'Atlético', + country: 'Spain', + }, + { + name: 'Atalanta', + country: 'Italy', + }, + { + name: 'Juventus', + country: 'Italy', + }, + { + name: 'Benfica', + country: 'Portugal', + }, + { + name: 'Arsenal', + country: 'England', + }, + { + name: 'Club Brugge', + country: 'Belgium', + }, + { + name: 'Rangers', + country: 'Scotland', + }, + { + name: 'Shakhtar', + country: 'Ukraine', + }, + ], + [ + { + name: 'Milan', + country: 'Italy', + }, + { + name: 'Feyenoord', + country: 'Netherlands', + }, + { + name: 'Sporting CP', + country: 'Portugal', + }, + { + name: 'PSV', + country: 'Netherlands', + }, + { + name: 'Slavia Praha', + country: 'Czech Republic', + }, + { + name: 'Dinamo Zagreb', + country: 'Croatia', + }, + { + name: 'Crvena zvezda', + country: 'Serbia', + }, + { + name: 'PAOK', + country: 'Greece', + }, + { + name: 'M Tel-Aviv', + country: 'Israel', + }, + ], + [ + { + name: 'Ferencváros', + country: 'Hungary', + }, + { + name: 'Celtic', + country: 'Scotland', + }, + { + name: 'Monaco', + country: 'France', + }, + { + name: 'Aston Villa', + country: 'England', + }, + { + name: 'Bologna', + country: 'Italy', + }, + { + name: 'Girona', + country: 'Spain', + }, + { + name: 'Stuttgart', + country: 'Germany', + }, + { + name: 'Sturm', + country: 'Austria', + }, + { + name: 'Brest', + country: 'France', + }, + ], +] satisfies readonly (readonly { + name: string; + country: string; +}[])[]; From 24a8af7fc7326c91ee2161d2df3155c3a6cd9ca2 Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Thu, 27 Jun 2024 10:00:07 +0100 Subject: [PATCH 09/41] delete --- src/experiments/generateMatchdays.ts | 320 --------------------------- 1 file changed, 320 deletions(-) delete mode 100644 src/experiments/generateMatchdays.ts diff --git a/src/experiments/generateMatchdays.ts b/src/experiments/generateMatchdays.ts deleted file mode 100644 index a3690fb2..00000000 --- a/src/experiments/generateMatchdays.ts +++ /dev/null @@ -1,320 +0,0 @@ -import { chunk, orderBy, shuffle } from 'lodash'; - -import { findFirstSolution } from '../utils/backtrack'; - -import generateFull from './generateFull'; - -const EASY_NUM_MATCHDAYS = 6; - -const generateMatchdays = ({ - teams, - numPots, - numMatchdays, - maxHomeGamesVsPot, -}: { - teams: readonly number[]; - numPots: number; - numMatchdays: number; - maxHomeGamesVsPot: number; -}) => { - const numTeamsPerPot = teams.length / numPots; - const numGamesPerMatchday = teams.length / 2; - - const maxGamesAtHome = Math.ceil(numMatchdays / 2); - - console.log('doing for', numMatchdays); - - const foo = new Set( - numMatchdays > EASY_NUM_MATCHDAYS - ? generateMatchdays({ - teams, - numPots, - numMatchdays: numMatchdays - 1, - maxHomeGamesVsPot, - }).flatMap(md => orderBy(md, m => Math.min(m[0], m[1]))) - : [], - ); - - let remainingGames = generateFull(teams); - - remainingGames = orderBy( - shuffle([ - ...remainingGames, - ...remainingGames.map(([a, b]) => [b, a] as [number, number]), - ]), - [ - // () => Math.random(), - m => (foo.has(m) ? 0 : 1), - ([a, b]) => { - if (a % 2 === 0 && b - a === 1) { - return 0.0000000001 * a; - } - if ((a - b === 3 && b % 4 === 0) || (a - b === 1 && b % 4 === 1)) { - return 0.0001 * a; - } - return Number.POSITIVE_INFINITY; - }, - ([a, b]) => -Math.abs(a - b), - ], - ); - - console.log('initial games', JSON.stringify(remainingGames)); - - const matches: [number, number][] = []; - const numHomeGamesByTeam: Record = {}; - const numAwayGamesByTeam: Record = {}; - - /** - * team:pot:home? - */ - const hasPlayedWithPotMap: Record< - `${number}:${number}:${'h' | 'a'}`, - number - > = {}; - - while (matches.length < numMatchdays * numGamesPerMatchday) { - console.log('nice'); - // remainingGames = shuffle(remainingGames); - - console.log(numHomeGamesByTeam, numAwayGamesByTeam, hasPlayedWithPotMap); - - console.log('orderedRemainingGames', [...remainingGames]); - - // eslint-disable-next-line no-loop-func - const pickedMatch = remainingGames.find(m => { - console.log('test...', m, { - remainingGames: [...remainingGames], - matches: [...matches], - }); - const solution = findFirstSolution( - { - source: remainingGames, - sourceStartFromIndex: 0, - target: matches, - numHomeGamesByTeam, - numAwayGamesByTeam, - hasPlayedWithPotMap, - picked: m, - }, - { - reject: c => { - // console.log('c', c); - const [m1, m2] = c.picked; - const matches = c.target; - - // Ensure the teams play same number of games at home & away - if (c.numHomeGamesByTeam[m1] === maxGamesAtHome) { - return true; - } - if (c.numAwayGamesByTeam[m2] === maxGamesAtHome) { - return true; - } - - // Ensure neither team has already played on this matchday - const matchdayIndex = Math.floor( - matches.length / numGamesPerMatchday, - ); - if ( - (c.numHomeGamesByTeam[m1] ?? 0) + - (c.numAwayGamesByTeam[m1] ?? 0) > - matchdayIndex - ) { - return true; - } - if ( - (c.numHomeGamesByTeam[m2] ?? 0) + - (c.numAwayGamesByTeam[m2] ?? 0) > - matchdayIndex - ) { - return true; - } - - // // Ensure the difference between home & away games is -1, 0 or 1 - // if ( - // (c.numHomeGamesByTeam[m1] ?? 0) > (c.numAwayGamesByTeam[m1] ?? 0) - // ) { - // return true; - // } - - // if ( - // (c.numAwayGamesByTeam[m2] ?? 0) > (c.numHomeGamesByTeam[m2] ?? 0) - // ) { - // return true; - // } - - const homeTeamPotIndex = Math.floor(m1 / numTeamsPerPot); - const awayTeamPotIndex = Math.floor(m2 / numTeamsPerPot); - - if ( - c.hasPlayedWithPotMap[`${m1}:${awayTeamPotIndex}:h`] === - maxHomeGamesVsPot - ) { - return true; - } - - if ( - c.hasPlayedWithPotMap[`${m2}:${homeTeamPotIndex}:a`] === - maxHomeGamesVsPot - ) { - return true; - } - - return false; - }, - accept: c => - c.target.length === numMatchdays * numGamesPerMatchday - 1, - generate: c => { - const newTarget = [...c.target, c.picked]; - const newNumHomeGamesByTeam = { - ...c.numHomeGamesByTeam, - [c.picked[0]]: (c.numHomeGamesByTeam[c.picked[0]] ?? 0) + 1, - } as typeof c.numHomeGamesByTeam; - const newNumAwayGamesByTeam = { - ...c.numAwayGamesByTeam, - [c.picked[1]]: (c.numAwayGamesByTeam[c.picked[1]] ?? 0) + 1, - } as typeof c.numAwayGamesByTeam; - - const pickedHomePotIndex = Math.floor(c.picked[0] / numTeamsPerPot); - const pickedAwayPotIndex = Math.floor(c.picked[1] / numTeamsPerPot); - - const newHasPlayedWithPotMap: typeof c.hasPlayedWithPotMap = { - ...c.hasPlayedWithPotMap, - [`${c.picked[0]}:${pickedAwayPotIndex}:h`]: - (c.hasPlayedWithPotMap[ - `${c.picked[0]}:${pickedAwayPotIndex}:h` - ] ?? 0) + 1, - [`${c.picked[1]}:${pickedHomePotIndex}:a`]: - (c.hasPlayedWithPotMap[ - `${c.picked[1]}:${pickedHomePotIndex}:a` - ] ?? 0) + 1, - } satisfies typeof c.hasPlayedWithPotMap; - - const oldMatchdayIndex = Math.floor( - c.target.length / numGamesPerMatchday, - ); - - const newMatchdayIndex = Math.floor( - newTarget.length / numGamesPerMatchday, - ); - - const newSource = c.source.filter(m => { - if ( - (m[0] === c.picked[0] && m[1] === c.picked[1]) || - (m[0] === c.picked[1] && m[1] === c.picked[0]) - ) { - return false; - } - if (newNumHomeGamesByTeam[m[0]] === maxGamesAtHome) { - return false; - } - if (newNumAwayGamesByTeam[m[1]] === maxGamesAtHome) { - return false; - } - - const homePot = Math.floor(m[0] / numTeamsPerPot); - const awayPot = Math.floor(m[1] / numTeamsPerPot); - - if ( - hasPlayedWithPotMap[`${m[0]}:${awayPot}:h`] === - maxHomeGamesVsPot - ) { - return false; - } - - if ( - hasPlayedWithPotMap[`${m[1]}:${homePot}:a`] === - maxHomeGamesVsPot - ) { - return false; - } - - return true; - }); - - const bannedMatches = c.source.slice(0, c.sourceStartFromIndex); - const bannedMatchesSet = new Set(bannedMatches); - - const candidates: (typeof c)[] = []; - - for (let i = 0; i < newSource.length; ++i) { - const newPicked = newSource[i]; - if (bannedMatchesSet.has(newPicked)) { - continue; - } - candidates.push({ - source: newSource, - sourceStartFromIndex: - newMatchdayIndex === oldMatchdayIndex ? i : 0, - target: newTarget, - picked: newPicked, - numHomeGamesByTeam: newNumHomeGamesByTeam, - numAwayGamesByTeam: newNumAwayGamesByTeam, - hasPlayedWithPotMap: newHasPlayedWithPotMap, - }); - } - - return candidates; - }, - }, - ); - if (!solution) { - console.log('sol', solution); - } - // console.log('sol', solution); - return solution !== undefined; - })!; - console.log('taking', pickedMatch); - - matches.push(pickedMatch); - - numHomeGamesByTeam[pickedMatch[0]] = - (numHomeGamesByTeam[pickedMatch[0]] ?? 0) + 1; - numAwayGamesByTeam[pickedMatch[1]] = - (numAwayGamesByTeam[pickedMatch[1]] ?? 0) + 1; - - const pickedHomePot = Math.floor(pickedMatch[0] / numTeamsPerPot); - const pickedAwayPot = Math.floor(pickedMatch[1] / numTeamsPerPot); - hasPlayedWithPotMap[`${pickedMatch[0]}:${pickedAwayPot}:h`] = - (hasPlayedWithPotMap[`${pickedMatch[0]}:${pickedAwayPot}:h`] ?? 0) + 1; - hasPlayedWithPotMap[`${pickedMatch[1]}:${pickedHomePot}:a`] = - (hasPlayedWithPotMap[`${pickedMatch[1]}:${pickedHomePot}:a`] ?? 0) + 1; - - console.log(numHomeGamesByTeam, numAwayGamesByTeam); - - remainingGames = remainingGames.filter(([a, b]) => { - const justPicked = - (a === pickedMatch[0] && b === pickedMatch[1]) || - (a === pickedMatch[1] && b === pickedMatch[0]); - if (justPicked) { - return false; - } - // if (numHomeGamesByTeam[a] === maxGamesAtHome) { - // return false; - // } - // if (numAwayGamesByTeam[b] === maxGamesAtHome) { - // return false; - // } - - const aPot = Math.floor(a / numTeamsPerPot); - const bPot = Math.floor(b / numTeamsPerPot); - - if (hasPlayedWithPotMap[`${a}:${bPot}:h`] === maxHomeGamesVsPot) { - return false; - } - - if (hasPlayedWithPotMap[`${b}:${aPot}:a`] === maxHomeGamesVsPot) { - return false; - } - - return true; - }); - console.log('new remaining', remainingGames); - console.log('current total', matches.length); - } - - console.log('done for num matchdays:', numMatchdays); - - return chunk(matches, numGamesPerMatchday); -}; - -export default generateMatchdays; From 2c87b3211f047ea18524cf1fe047a682dc8b0653 Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Thu, 27 Jun 2024 19:25:53 +0100 Subject: [PATCH 10/41] worker --- src/experiments/generatePairings.ts | 7 +++--- .../getFirstSuitableMatch.worker.ts | 7 ++++++ .../getFirstSuitableMatch.wrapper.ts | 14 ++++++++++++ src/experiments/index.ts | 22 +++++++++---------- 4 files changed, 36 insertions(+), 14 deletions(-) create mode 100644 src/experiments/getFirstSuitableMatch.worker.ts create mode 100644 src/experiments/getFirstSuitableMatch.wrapper.ts diff --git a/src/experiments/generatePairings.ts b/src/experiments/generatePairings.ts index 1ae26577..f191cf43 100644 --- a/src/experiments/generatePairings.ts +++ b/src/experiments/generatePairings.ts @@ -1,9 +1,9 @@ import { orderBy, range, shuffle } from 'lodash'; import generateFull from './generateFull'; -import getFirstSuitableMatch from './getFirstSuitableMatch'; +import getFirstSuitableMatch from './getFirstSuitableMatch.wrapper'; -export default ({ +export default async ({ pots, numMatchdays, isMatchPossible, @@ -51,7 +51,8 @@ export default ({ console.log('nice'); // remainingGames = shuffle(remainingGames); - const pickedMatch = getFirstSuitableMatch({ + // eslint-disable-next-line no-await-in-loop + const pickedMatch = await getFirstSuitableMatch({ numPots: pots.length, numTeamsPerPot, numMatchdays, diff --git a/src/experiments/getFirstSuitableMatch.worker.ts b/src/experiments/getFirstSuitableMatch.worker.ts new file mode 100644 index 00000000..8b40a685 --- /dev/null +++ b/src/experiments/getFirstSuitableMatch.worker.ts @@ -0,0 +1,7 @@ +import exposeWorker, { type ExposedFuncType } from '#utils/worker/expose'; + +import getFirstSuitableMatch from './getFirstSuitableMatch'; + +export type Func = ExposedFuncType; + +exposeWorker(getFirstSuitableMatch); diff --git a/src/experiments/getFirstSuitableMatch.wrapper.ts b/src/experiments/getFirstSuitableMatch.wrapper.ts new file mode 100644 index 00000000..fbaca187 --- /dev/null +++ b/src/experiments/getFirstSuitableMatch.wrapper.ts @@ -0,0 +1,14 @@ +import workerSendAndReceive from '#utils/worker/sendAndReceive'; + +import { type Func } from './getFirstSuitableMatch.worker'; + +export default async (...args: Parameters) => { + const worker = new Worker( + new URL('./getFirstSuitableMatch.worker', import.meta.url), + ); + const result = (await workerSendAndReceive(worker)( + ...args, + )) as ReturnType; + worker.terminate(); + return result; +}; diff --git a/src/experiments/index.ts b/src/experiments/index.ts index af1816f4..1d26b37d 100644 --- a/src/experiments/index.ts +++ b/src/experiments/index.ts @@ -1,5 +1,3 @@ -import { chunk, range, stubTrue } from 'lodash'; - import generatePairings from './generatePairings'; import pots from './pots'; @@ -11,12 +9,14 @@ function canPlay(a: Team, b: Team) { return a.country !== b.country; } -const matchdays = generatePairings({ - pots, - numMatchdays: NUM_MATCHDAYS, - isMatchPossible: canPlay, -}); -console.log( - 'final', - matchdays.map(m => [m[0].name, m[1].name]), -); +(async () => { + const matchdays = await generatePairings({ + pots, + numMatchdays: NUM_MATCHDAYS, + isMatchPossible: canPlay, + }); + console.log( + 'final', + matchdays.map(m => [m[0].name, m[1].name]), + ); +})(); From a7ce7dcad6c9ac33fc7dcf420576b5567aff968a Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Fri, 28 Jun 2024 15:47:48 +0100 Subject: [PATCH 11/41] fix --- src/experiments/pots.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/experiments/pots.ts b/src/experiments/pots.ts index 504da16d..e6b6e736 100644 --- a/src/experiments/pots.ts +++ b/src/experiments/pots.ts @@ -94,7 +94,7 @@ export default [ }, { name: 'Slavia Praha', - country: 'Czech Republic', + country: 'Czechia', }, { name: 'Dinamo Zagreb', From d96fd19089da2b7355247bc65181b5154fe1b4bc Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Fri, 28 Jun 2024 15:48:01 +0100 Subject: [PATCH 12/41] del groups --- src/experiments/groups.ts | 91 --------------------------------------- 1 file changed, 91 deletions(-) delete mode 100644 src/experiments/groups.ts diff --git a/src/experiments/groups.ts b/src/experiments/groups.ts deleted file mode 100644 index 0d1cd210..00000000 --- a/src/experiments/groups.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { chunk, pull, range, shuffle } from 'lodash'; - -import { findFirstSolution } from '#utils/backtrack.js'; - -const teams = range(36).map(i => ({ id: i + 1 })); -const numPots = 4; - -type Team = (typeof teams)[number]; - -const pots = chunk(teams, teams.length / numPots); - -const pairedPots: Record<`${number}:${number}`, [Team[], Team[]]> = {}; - -const totalMatches = teams.length * numPots; - -for (let i = 0; i < pots.length; ++i) { - for (let j = 0; j < pots.length; ++j) { - pairedPots[`${i}:${j}`] = [pots[i], pots[j]]; - } -} - -const resultingMatches: [Team, Team][] = []; - -while (true) { - for (let i = 0; i < pots.length; ++i) { - for (let j = 0; j < pots.length; ++j) { - const [remainingHomePot, remainingAwayPot] = pairedPots[`${i}:${j}`]; - let isHomeBeingPicked = true; - while (remainingHomePot.length > 0 || remainingAwayPot.length > 0) { - const potToPickFrom = isHomeBeingPicked - ? remainingHomePot - : remainingAwayPot; - - // eslint-disable-next-line no-loop-func - const pickedTeam = potToPickFrom.find(team => { - const solution = findFirstSolution( - { - pairedPots, - matches: resultingMatches, - picked: team, - currentHomePot: i, - currentAwayPot: j, - isHomeBeingPicked, - }, - { - // eslint-disable-next-line arrow-body-style - reject: c => { - return false; - }, - accept: c => c.matches.length === totalMatches - 1, - generate: c => { - const newPotPair = [ - ...c.pairedPots[`${c.currentHomePot}:${c.currentAwayPot}`], - ] as [Team[], Team[]]; - const index = c.isHomeBeingPicked ? 0 : 1; - newPotPair[index] = [...newPotPair[index]].filter( - item => item !== c.picked, - ); - - const newPairedPots = { - ...c.pairedPots, - [`${c.currentHomePot}:${c.currentAwayPot}`]: newPotPair, - } as typeof c.pairedPots; - - const newMatches = [...c.matches]; - if (c.isHomeBeingPicked) { - newMatches.push([c.picked] as unknown as [Team, Team]); - } else { - newMatches[newMatches.length - 1] = [ - newMatches.at(-1)![0], - c.picked, - ]; - } - - const newIsHomeBeingPicked = !c.isHomeBeingPicked - - if () - }, - }, - ); - - return solution !== undefined; - }); - - isHomeBeingPicked = !isHomeBeingPicked; - } - } - } -} - -console.log('resulting matches', resultingMatches); From 5ab3a679668940680b13f53e3f243eb5a15ac4ac Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Sat, 29 Jun 2024 21:18:39 +0100 Subject: [PATCH 13/41] upd --- src/experiments/generatePairings.ts | 19 +- src/experiments/generateSchedule.ts | 184 ++++++ src/experiments/getFirstSuitableMatch.ts | 19 +- .../getFirstSuitableMatch.wrapper.ts | 78 ++- src/experiments/index.ts | 10 +- src/index.tsx | 2 +- src/pages/league/games.ts | 610 ++++++++++++++++++ src/pages/league/index.tsx | 286 ++++++++ src/routes/index.tsx | 5 + 9 files changed, 1181 insertions(+), 32 deletions(-) create mode 100644 src/experiments/generateSchedule.ts create mode 100644 src/pages/league/games.ts create mode 100644 src/pages/league/index.tsx diff --git a/src/experiments/generatePairings.ts b/src/experiments/generatePairings.ts index f191cf43..9ebe0fc1 100644 --- a/src/experiments/generatePairings.ts +++ b/src/experiments/generatePairings.ts @@ -3,7 +3,7 @@ import { orderBy, range, shuffle } from 'lodash'; import generateFull from './generateFull'; import getFirstSuitableMatch from './getFirstSuitableMatch.wrapper'; -export default async ({ +export default async function* generatePairings({ pots, numMatchdays, isMatchPossible, @@ -11,8 +11,7 @@ export default async ({ pots: readonly (readonly T[])[]; numMatchdays: number; isMatchPossible: (h: T, a: T) => boolean; -}) => { - console.log(JSON.stringify(pots)); +}) { const teams = pots.flat(); const numTeamsPerPot = pots[0].length; const numGamesPerMatchday = teams.length / 2; @@ -48,9 +47,6 @@ export default async ({ const matches: (readonly [number, number])[] = []; while (matches.length < numMatchdays * numGamesPerMatchday) { - console.log('nice'); - // remainingGames = shuffle(remainingGames); - // eslint-disable-next-line no-await-in-loop const pickedMatch = await getFirstSuitableMatch({ numPots: pots.length, @@ -60,15 +56,8 @@ export default async ({ allGames, pickedMatches: matches, }); - - console.log('taking', pickedMatch); - matches.push(pickedMatch); - console.log('current total', matches.length); + yield [teams[pickedMatch[0]], teams[pickedMatch[1]]] as const; } - - console.log('done for num matchdays:', numMatchdays); - - return matches.map(([h, a]) => [teams[h], teams[a]] as const); -}; +} diff --git a/src/experiments/generateSchedule.ts b/src/experiments/generateSchedule.ts new file mode 100644 index 00000000..79e1104e --- /dev/null +++ b/src/experiments/generateSchedule.ts @@ -0,0 +1,184 @@ +import { keyBy, orderBy } from 'lodash'; + +import { findFirstSolution } from '#utils/backtrack'; + +const getFirstSuitableMatch = ({ + matchdaySize, + remainingMatches: remainingMatchesUnordered, + schedule, +}: { + matchdaySize: number; + remainingMatches: readonly (readonly [number, number])[]; + schedule: readonly (readonly [number, number])[]; +}) => { + // team:md + const locationByMatchday: Record<`${number}:${number}`, boolean> = {}; + const numHomeGamesByTeam: Record = {}; + const numAwayGamesByTeam: Record = {}; + + const remainingMatches = orderBy(remainingMatchesUnordered, [ + m => m[0], + m => -m[1], + ]); + + debugger; + + for (const [i, [h, a]] of schedule.entries()) { + const matchdayIndex = Math.floor(i / matchdaySize); + locationByMatchday[`${h}:${matchdayIndex}`] = true; + locationByMatchday[`${a}:${matchdayIndex}`] = true; + numHomeGamesByTeam[h] = (numHomeGamesByTeam[h] ?? 0) + 1; + numAwayGamesByTeam[a] = (numAwayGamesByTeam[a] ?? 0) + 1; + } + + return remainingMatches.find(m => { + const solution = findFirstSolution( + { + source: remainingMatches, + target: schedule, + picked: m, + locationByMatchday, + numHomeGamesByTeam, + numAwayGamesByTeam, + }, + { + reject: c => { + const [h, a] = c.picked; + const currentMatchdayIndex = Math.floor( + c.target.length / matchdaySize, + ); + + const hasHomeTeamPlayedThisMatchday = + c.locationByMatchday[`${h}:${currentMatchdayIndex}`]; + if (hasHomeTeamPlayedThisMatchday) { + return true; + } + + const hasAwayTeamPlayedThisMatchday = + c.locationByMatchday[`${a}:${currentMatchdayIndex}`]; + if (hasAwayTeamPlayedThisMatchday) { + return true; + } + + if (c.numHomeGamesByTeam[h] > c.numAwayGamesByTeam[h]) { + return true; + } + + if (c.numAwayGamesByTeam[a] > c.numHomeGamesByTeam[a]) { + return true; + } + + return false; + }, + + accept: c => c.source.length === 1, + + generate: c => { + const oldMatchdayIndex = Math.floor(c.target.length / matchdaySize); + + const newTarget = [...c.target, c.picked]; + const newMatchdayIndex = Math.floor(newTarget.length / matchdaySize); + const newLocationByMatchday: typeof c.locationByMatchday = { + ...c.locationByMatchday, + [`${c.picked[0]}:${oldMatchdayIndex}`]: true, + [`${c.picked[1]}:${oldMatchdayIndex}`]: true, + } satisfies typeof c.locationByMatchday; + + const newNumHomeGamesByTeam = { + ...c.numHomeGamesByTeam, + [c.picked[0]]: (c.numHomeGamesByTeam[c.picked[0]] ?? 0) + 1, + } as typeof c.numHomeGamesByTeam; + const newNumAwayGamesByTeam = { + ...c.numAwayGamesByTeam, + [c.picked[1]]: (c.numAwayGamesByTeam[c.picked[1]] ?? 0) + 1, + } as typeof c.numAwayGamesByTeam; + + const newSource = c.source.filter(m => m !== c.picked); + + const thisMatchday = newTarget.slice(newMatchdayIndex * matchdaySize); + + const highestHomeThisMatchday = thisMatchday + // eslint-disable-next-line unicorn/no-array-reduce + .reduce((prev, cur) => Math.max(prev, cur[0]), -1); + + const foo = newSource.filter(([h, a]) => { + if (h <= highestHomeThisMatchday) { + return false; + } + + if (newLocationByMatchday[`${h}:${newMatchdayIndex}`]) { + return false; + } + + if (newLocationByMatchday[`${a}:${newMatchdayIndex}`]) { + return false; + } + + return true; + }); + + const uniqueHomeTeams = new Set(foo.map(m => m[0])); + + // console.log(newTarget.length); + + const candidates = []; + + const newMatchdaySize = newTarget.length % matchdaySize; + + if (newMatchdaySize + uniqueHomeTeams.size >= matchdaySize) { + for (const newPicked of foo) { + candidates.push({ + source: newSource, + target: newTarget, + picked: newPicked, + locationByMatchday: newLocationByMatchday, + numHomeGamesByTeam: newNumHomeGamesByTeam, + numAwayGamesByTeam: newNumAwayGamesByTeam, + }); + } + } + + return candidates; + }, + }, + ); + + if (!solution) { + console.log('sol', solution); + } + // console.log('sol', solution); + return solution !== undefined; + })!; +}; + +export default function* generateSchedule({ + matchdaySize, + matches, +}: { + matchdaySize: number; + matches: readonly (readonly [T, T])[]; +}) { + const foo = matches.flat(); + const aaa = keyBy(foo, team => team.id); + const allTeamIds = [...new Set(foo.map(team => team.id))]; + const indexByTeamId = new Map(allTeamIds.map((id, i) => [id, i] as const)); + + const schedule: (readonly [number, number])[] = []; + const remainingMatches = matches.map( + ([h, a]) => [indexByTeamId.get(h.id)!, indexByTeamId.get(a.id)!] as const, + ); + + while (remainingMatches.length > 0) { + const pickedMatch = getFirstSuitableMatch({ + matchdaySize, + remainingMatches, + schedule, + }); + console.log('picked match schedule', pickedMatch); + schedule.push(pickedMatch); + remainingMatches.splice(remainingMatches.indexOf(pickedMatch), 1); + + yield [aaa[allTeamIds[0]], aaa[allTeamIds[1]]] as const; + } + debugger; +} diff --git a/src/experiments/getFirstSuitableMatch.ts b/src/experiments/getFirstSuitableMatch.ts index 424d5af1..75624228 100644 --- a/src/experiments/getFirstSuitableMatch.ts +++ b/src/experiments/getFirstSuitableMatch.ts @@ -1,4 +1,4 @@ -import { shuffle } from 'lodash'; +import { orderBy, shuffle } from 'lodash'; import { findFirstSolution } from '../utils/backtrack'; @@ -9,6 +9,8 @@ export default ({ numGamesPerMatchday, allGames, pickedMatches, + randomArray, + shouldShuffle, }: { numPots: number; numTeamsPerPot: number; @@ -16,6 +18,8 @@ export default ({ numGamesPerMatchday: number; allGames: readonly (readonly [number, number])[]; pickedMatches: readonly (readonly [number, number])[]; + randomArray: readonly number[]; + shouldShuffle: boolean; }) => { const maxGamesAtHome = Math.ceil(numMatchdays / 2); @@ -72,11 +76,20 @@ export default ({ console.log('num remaining possible games', remainingGames.length); - return shuffle(remainingGames).find(m => { + const orderedRemainingGames = orderBy( + remainingGames, + (_, i) => randomArray[i], + ); + + const shuffledRemainingGames = shouldShuffle + ? shuffle(remainingGames) + : remainingGames; + + return orderedRemainingGames.find(m => { console.log('test...', m); const solution = findFirstSolution( { - source: remainingGames, + source: shuffledRemainingGames, target: pickedMatches, numHomeGamesByTeam, numAwayGamesByTeam, diff --git a/src/experiments/getFirstSuitableMatch.wrapper.ts b/src/experiments/getFirstSuitableMatch.wrapper.ts index fbaca187..9b0d93d6 100644 --- a/src/experiments/getFirstSuitableMatch.wrapper.ts +++ b/src/experiments/getFirstSuitableMatch.wrapper.ts @@ -1,14 +1,76 @@ +import delay from 'delay.js'; + import workerSendAndReceive from '#utils/worker/sendAndReceive'; import { type Func } from './getFirstSuitableMatch.worker'; -export default async (...args: Parameters) => { - const worker = new Worker( - new URL('./getFirstSuitableMatch.worker', import.meta.url), +const NUM_WORKERS = navigator.hardwareConcurrency - 1; + +const fastest: Record = Object.fromEntries( + Array.from( + { + length: NUM_WORKERS, + }, + (_, i) => [i, 0] as const, + ), +); + +export default async ( + options: Omit[0], 'randomArray' | 'shouldShuffle'>, +) => { + const randomArray = Array.from( + { + length: options.allGames.length, + }, + () => Math.random(), ); - const result = (await workerSendAndReceive(worker)( - ...args, - )) as ReturnType; - worker.terminate(); - return result; + + const firstResult = await new Promise<{ + index: number; + result: Awaited>; + }>(async resolve => { + const workers: Worker[] = []; + let isLoopRunning = true; + for (let i = 0; isLoopRunning; ++i) { + console.log('length', workers.length); + if (workers.length >= NUM_WORKERS) { + const oldestWorker = workers.shift(); + oldestWorker?.terminate(); + } + + const worker = new Worker( + new URL('./getFirstSuitableMatch.worker', import.meta.url), + ); + workers.push(worker); + + // eslint-disable-next-line no-loop-func + (async () => { + const result = await workerSendAndReceive< + Parameters[0], + ReturnType + >(worker)({ + ...options, + randomArray, + shouldShuffle: i > 0, + }); + isLoopRunning = false; + for (const w of workers) { + w.terminate(); + } + resolve({ + result, + index: i, + }); + })(); + + // eslint-disable-next-line no-await-in-loop + await delay(500 * Math.exp(i / 10)); + } + }); + + ++fastest[firstResult.index]; + + console.log('fastest worker', fastest); + + return firstResult.result; }; diff --git a/src/experiments/index.ts b/src/experiments/index.ts index 1d26b37d..9d30fb3f 100644 --- a/src/experiments/index.ts +++ b/src/experiments/index.ts @@ -10,11 +10,11 @@ function canPlay(a: Team, b: Team) { } (async () => { - const matchdays = await generatePairings({ - pots, - numMatchdays: NUM_MATCHDAYS, - isMatchPossible: canPlay, - }); + // const matchdays = await generatePairings({ + // pots, + // numMatchdays: NUM_MATCHDAYS, + // isMatchPossible: canPlay, + // }); console.log( 'final', matchdays.map(m => [m[0].name, m[1].name]), diff --git a/src/index.tsx b/src/index.tsx index 6fa3219e..9d871b65 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -8,5 +8,5 @@ const root = createRoot(container); root.render(); setTimeout(() => { - import('./experiments'); + // import('./experiments'); }, 2000); diff --git a/src/pages/league/games.ts b/src/pages/league/games.ts new file mode 100644 index 00000000..b648fd77 --- /dev/null +++ b/src/pages/league/games.ts @@ -0,0 +1,610 @@ +export default [ + [ + { name: 'Internazionale', country: 'Italy', id: 'Italy:Internazionale' }, + { name: 'Paris', country: 'France', id: 'France:Paris' }, + ], + [ + { name: 'Real Madrid', country: 'Spain', id: 'Spain:Real Madrid' }, + { + name: 'Slavia Praha', + country: 'Czech Republic', + id: 'Czech Republic:Slavia Praha', + }, + ], + [ + { name: 'Bayern', country: 'Germany', id: 'Germany:Bayern' }, + { name: 'PSV', country: 'Netherlands', id: 'Netherlands:PSV' }, + ], + [ + { name: 'Dinamo Zagreb', country: 'Croatia', id: 'Croatia:Dinamo Zagreb' }, + { name: 'Sturm', country: 'Austria', id: 'Austria:Sturm' }, + ], + [ + { name: 'Shakhtar', country: 'Ukraine', id: 'Ukraine:Shakhtar' }, + { name: 'Girona', country: 'Spain', id: 'Spain:Girona' }, + ], + [ + { name: 'PSV', country: 'Netherlands', id: 'Netherlands:PSV' }, + { name: 'Monaco', country: 'France', id: 'France:Monaco' }, + ], + [ + { name: 'Benfica', country: 'Portugal', id: 'Portugal:Benfica' }, + { name: 'Shakhtar', country: 'Ukraine', id: 'Ukraine:Shakhtar' }, + ], + [ + { name: 'Sporting CP', country: 'Portugal', id: 'Portugal:Sporting CP' }, + { name: 'Barcelona', country: 'Spain', id: 'Spain:Barcelona' }, + ], + [ + { name: 'Brest', country: 'France', id: 'France:Brest' }, + { name: 'Milan', country: 'Italy', id: 'Italy:Milan' }, + ], + [ + { name: 'Paris', country: 'France', id: 'France:Paris' }, + { name: 'Ferencváros', country: 'Hungary', id: 'Hungary:Ferencváros' }, + ], + [ + { name: 'PAOK', country: 'Greece', id: 'Greece:PAOK' }, + { name: 'Man City', country: 'England', id: 'England:Man City' }, + ], + [ + { name: 'Ferencváros', country: 'Hungary', id: 'Hungary:Ferencváros' }, + { name: 'Sturm', country: 'Austria', id: 'Austria:Sturm' }, + ], + [ + { name: 'Liverpool', country: 'England', id: 'England:Liverpool' }, + { name: 'Bologna', country: 'Italy', id: 'Italy:Bologna' }, + ], + [ + { name: 'Rangers', country: 'Scotland', id: 'Scotland:Rangers' }, + { name: 'Leverkusen', country: 'Germany', id: 'Germany:Leverkusen' }, + ], + [ + { name: 'Paris', country: 'France', id: 'France:Paris' }, + { name: 'Leipzig', country: 'Germany', id: 'Germany:Leipzig' }, + ], + [ + { name: 'Atalanta', country: 'Italy', id: 'Italy:Atalanta' }, + { name: 'Club Brugge', country: 'Belgium', id: 'Belgium:Club Brugge' }, + ], + [ + { name: 'Leipzig', country: 'Germany', id: 'Germany:Leipzig' }, + { name: 'Aston Villa', country: 'England', id: 'England:Aston Villa' }, + ], + [ + { name: 'Leipzig', country: 'Germany', id: 'Germany:Leipzig' }, + { name: 'PAOK', country: 'Greece', id: 'Greece:PAOK' }, + ], + [ + { name: 'Shakhtar', country: 'Ukraine', id: 'Ukraine:Shakhtar' }, + { name: 'Sporting CP', country: 'Portugal', id: 'Portugal:Sporting CP' }, + ], + [ + { name: 'Stuttgart', country: 'Germany', id: 'Germany:Stuttgart' }, + { name: 'Girona', country: 'Spain', id: 'Spain:Girona' }, + ], + [ + { name: 'Leverkusen', country: 'Germany', id: 'Germany:Leverkusen' }, + { name: 'Benfica', country: 'Portugal', id: 'Portugal:Benfica' }, + ], + [ + { + name: 'Slavia Praha', + country: 'Czech Republic', + id: 'Czech Republic:Slavia Praha', + }, + { name: 'Dinamo Zagreb', country: 'Croatia', id: 'Croatia:Dinamo Zagreb' }, + ], + [ + { name: 'M Tel-Aviv', country: 'Israel', id: 'Israel:M Tel-Aviv' }, + { name: 'Shakhtar', country: 'Ukraine', id: 'Ukraine:Shakhtar' }, + ], + [ + { name: 'Bologna', country: 'Italy', id: 'Italy:Bologna' }, + { name: 'Arsenal', country: 'England', id: 'England:Arsenal' }, + ], + [ + { name: 'Crvena zvezda', country: 'Serbia', id: 'Serbia:Crvena zvezda' }, + { name: 'Leipzig', country: 'Germany', id: 'Germany:Leipzig' }, + ], + [ + { name: 'Barcelona', country: 'Spain', id: 'Spain:Barcelona' }, + { name: 'Leverkusen', country: 'Germany', id: 'Germany:Leverkusen' }, + ], + [ + { name: 'Sporting CP', country: 'Portugal', id: 'Portugal:Sporting CP' }, + { name: 'Leverkusen', country: 'Germany', id: 'Germany:Leverkusen' }, + ], + [ + { name: 'Crvena zvezda', country: 'Serbia', id: 'Serbia:Crvena zvezda' }, + { name: 'PSV', country: 'Netherlands', id: 'Netherlands:PSV' }, + ], + [ + { name: 'Leverkusen', country: 'Germany', id: 'Germany:Leverkusen' }, + { name: 'Man City', country: 'England', id: 'England:Man City' }, + ], + [ + { name: 'Crvena zvezda', country: 'Serbia', id: 'Serbia:Crvena zvezda' }, + { name: 'Brest', country: 'France', id: 'France:Brest' }, + ], + [ + { name: 'Shakhtar', country: 'Ukraine', id: 'Ukraine:Shakhtar' }, + { name: 'Leipzig', country: 'Germany', id: 'Germany:Leipzig' }, + ], + [ + { name: 'Shakhtar', country: 'Ukraine', id: 'Ukraine:Shakhtar' }, + { name: 'Rangers', country: 'Scotland', id: 'Scotland:Rangers' }, + ], + [ + { name: 'Bayern', country: 'Germany', id: 'Germany:Bayern' }, + { name: 'Liverpool', country: 'England', id: 'England:Liverpool' }, + ], + [ + { name: 'Brest', country: 'France', id: 'France:Brest' }, + { name: 'Leipzig', country: 'Germany', id: 'Germany:Leipzig' }, + ], + [ + { name: 'Dinamo Zagreb', country: 'Croatia', id: 'Croatia:Dinamo Zagreb' }, + { name: 'Liverpool', country: 'England', id: 'England:Liverpool' }, + ], + [ + { name: 'Club Brugge', country: 'Belgium', id: 'Belgium:Club Brugge' }, + { name: 'Stuttgart', country: 'Germany', id: 'Germany:Stuttgart' }, + ], + [ + { name: 'Internazionale', country: 'Italy', id: 'Italy:Internazionale' }, + { name: 'Girona', country: 'Spain', id: 'Spain:Girona' }, + ], + [ + { name: 'Girona', country: 'Spain', id: 'Spain:Girona' }, + { name: 'PAOK', country: 'Greece', id: 'Greece:PAOK' }, + ], + [ + { name: 'Juventus', country: 'Italy', id: 'Italy:Juventus' }, + { name: 'Celtic', country: 'Scotland', id: 'Scotland:Celtic' }, + ], + [ + { name: 'Man City', country: 'England', id: 'England:Man City' }, + { name: 'Shakhtar', country: 'Ukraine', id: 'Ukraine:Shakhtar' }, + ], + [ + { name: 'Benfica', country: 'Portugal', id: 'Portugal:Benfica' }, + { name: 'Dortmund', country: 'Germany', id: 'Germany:Dortmund' }, + ], + [ + { name: 'Monaco', country: 'France', id: 'France:Monaco' }, + { name: 'Liverpool', country: 'England', id: 'England:Liverpool' }, + ], + [ + { name: 'Sturm', country: 'Austria', id: 'Austria:Sturm' }, + { name: 'Monaco', country: 'France', id: 'France:Monaco' }, + ], + [ + { name: 'Ferencváros', country: 'Hungary', id: 'Hungary:Ferencváros' }, + { name: 'PSV', country: 'Netherlands', id: 'Netherlands:PSV' }, + ], + [ + { name: 'Sturm', country: 'Austria', id: 'Austria:Sturm' }, + { name: 'Leverkusen', country: 'Germany', id: 'Germany:Leverkusen' }, + ], + [ + { name: 'Atlético', country: 'Spain', id: 'Spain:Atlético' }, + { name: 'Crvena zvezda', country: 'Serbia', id: 'Serbia:Crvena zvezda' }, + ], + [ + { name: 'Rangers', country: 'Scotland', id: 'Scotland:Rangers' }, + { + name: 'Slavia Praha', + country: 'Czech Republic', + id: 'Czech Republic:Slavia Praha', + }, + ], + [ + { name: 'Juventus', country: 'Italy', id: 'Italy:Juventus' }, + { name: 'Dinamo Zagreb', country: 'Croatia', id: 'Croatia:Dinamo Zagreb' }, + ], + [ + { name: 'Feyenoord', country: 'Netherlands', id: 'Netherlands:Feyenoord' }, + { name: 'Benfica', country: 'Portugal', id: 'Portugal:Benfica' }, + ], + [ + { name: 'Bologna', country: 'Italy', id: 'Italy:Bologna' }, + { name: 'M Tel-Aviv', country: 'Israel', id: 'Israel:M Tel-Aviv' }, + ], + [ + { name: 'Arsenal', country: 'England', id: 'England:Arsenal' }, + { name: 'Atalanta', country: 'Italy', id: 'Italy:Atalanta' }, + ], + [ + { name: 'Dortmund', country: 'Germany', id: 'Germany:Dortmund' }, + { name: 'Juventus', country: 'Italy', id: 'Italy:Juventus' }, + ], + [ + { name: 'Club Brugge', country: 'Belgium', id: 'Belgium:Club Brugge' }, + { name: 'Juventus', country: 'Italy', id: 'Italy:Juventus' }, + ], + [ + { name: 'PSV', country: 'Netherlands', id: 'Netherlands:PSV' }, + { name: 'Sporting CP', country: 'Portugal', id: 'Portugal:Sporting CP' }, + ], + [ + { name: 'Monaco', country: 'France', id: 'France:Monaco' }, + { name: 'Atlético', country: 'Spain', id: 'Spain:Atlético' }, + ], + [ + { name: 'Celtic', country: 'Scotland', id: 'Scotland:Celtic' }, + { name: 'Club Brugge', country: 'Belgium', id: 'Belgium:Club Brugge' }, + ], + [ + { name: 'Monaco', country: 'France', id: 'France:Monaco' }, + { name: 'Dinamo Zagreb', country: 'Croatia', id: 'Croatia:Dinamo Zagreb' }, + ], + [ + { name: 'Juventus', country: 'Italy', id: 'Italy:Juventus' }, + { name: 'Paris', country: 'France', id: 'France:Paris' }, + ], + [ + { name: 'Girona', country: 'Spain', id: 'Spain:Girona' }, + { name: 'Juventus', country: 'Italy', id: 'Italy:Juventus' }, + ], + [ + { name: 'Sturm', country: 'Austria', id: 'Austria:Sturm' }, + { name: 'Internazionale', country: 'Italy', id: 'Italy:Internazionale' }, + ], + [ + { name: 'Girona', country: 'Spain', id: 'Spain:Girona' }, + { name: 'Aston Villa', country: 'England', id: 'England:Aston Villa' }, + ], + [ + { name: 'Barcelona', country: 'Spain', id: 'Spain:Barcelona' }, + { name: 'Milan', country: 'Italy', id: 'Italy:Milan' }, + ], + [ + { name: 'Celtic', country: 'Scotland', id: 'Scotland:Celtic' }, + { name: 'Bayern', country: 'Germany', id: 'Germany:Bayern' }, + ], + [ + { name: 'Benfica', country: 'Portugal', id: 'Portugal:Benfica' }, + { name: 'Ferencváros', country: 'Hungary', id: 'Hungary:Ferencváros' }, + ], + [ + { name: 'Atlético', country: 'Spain', id: 'Spain:Atlético' }, + { name: 'Aston Villa', country: 'England', id: 'England:Aston Villa' }, + ], + [ + { name: 'Sporting CP', country: 'Portugal', id: 'Portugal:Sporting CP' }, + { name: 'M Tel-Aviv', country: 'Israel', id: 'Israel:M Tel-Aviv' }, + ], + [ + { name: 'Crvena zvezda', country: 'Serbia', id: 'Serbia:Crvena zvezda' }, + { name: 'Rangers', country: 'Scotland', id: 'Scotland:Rangers' }, + ], + [ + { name: 'PSV', country: 'Netherlands', id: 'Netherlands:PSV' }, + { name: 'Arsenal', country: 'England', id: 'England:Arsenal' }, + ], + [ + { name: 'Liverpool', country: 'England', id: 'England:Liverpool' }, + { name: 'Internazionale', country: 'Italy', id: 'Italy:Internazionale' }, + ], + [ + { name: 'Milan', country: 'Italy', id: 'Italy:Milan' }, + { name: 'Aston Villa', country: 'England', id: 'England:Aston Villa' }, + ], + [ + { name: 'Ferencváros', country: 'Hungary', id: 'Hungary:Ferencváros' }, + { name: 'Real Madrid', country: 'Spain', id: 'Spain:Real Madrid' }, + ], + [ + { name: 'Dortmund', country: 'Germany', id: 'Germany:Dortmund' }, + { name: 'Feyenoord', country: 'Netherlands', id: 'Netherlands:Feyenoord' }, + ], + [ + { name: 'Celtic', country: 'Scotland', id: 'Scotland:Celtic' }, + { name: 'Sporting CP', country: 'Portugal', id: 'Portugal:Sporting CP' }, + ], + [ + { name: 'Ferencváros', country: 'Hungary', id: 'Hungary:Ferencváros' }, + { name: 'Rangers', country: 'Scotland', id: 'Scotland:Rangers' }, + ], + [ + { name: 'Internazionale', country: 'Italy', id: 'Italy:Internazionale' }, + { name: 'Benfica', country: 'Portugal', id: 'Portugal:Benfica' }, + ], + [ + { + name: 'Slavia Praha', + country: 'Czech Republic', + id: 'Czech Republic:Slavia Praha', + }, + { name: 'Atalanta', country: 'Italy', id: 'Italy:Atalanta' }, + ], + [ + { name: 'M Tel-Aviv', country: 'Israel', id: 'Israel:M Tel-Aviv' }, + { + name: 'Slavia Praha', + country: 'Czech Republic', + id: 'Czech Republic:Slavia Praha', + }, + ], + [ + { name: 'Feyenoord', country: 'Netherlands', id: 'Netherlands:Feyenoord' }, + { name: 'Paris', country: 'France', id: 'France:Paris' }, + ], + [ + { name: 'Leipzig', country: 'Germany', id: 'Germany:Leipzig' }, + { name: 'Rangers', country: 'Scotland', id: 'Scotland:Rangers' }, + ], + [ + { name: 'PAOK', country: 'Greece', id: 'Greece:PAOK' }, + { name: 'Juventus', country: 'Italy', id: 'Italy:Juventus' }, + ], + [ + { name: 'M Tel-Aviv', country: 'Israel', id: 'Israel:M Tel-Aviv' }, + { name: 'Bayern', country: 'Germany', id: 'Germany:Bayern' }, + ], + [ + { + name: 'Slavia Praha', + country: 'Czech Republic', + id: 'Czech Republic:Slavia Praha', + }, + { name: 'Stuttgart', country: 'Germany', id: 'Germany:Stuttgart' }, + ], + [ + { name: 'Club Brugge', country: 'Belgium', id: 'Belgium:Club Brugge' }, + { name: 'Bayern', country: 'Germany', id: 'Germany:Bayern' }, + ], + [ + { name: 'Celtic', country: 'Scotland', id: 'Scotland:Celtic' }, + { name: 'Brest', country: 'France', id: 'France:Brest' }, + ], + [ + { name: 'Arsenal', country: 'England', id: 'England:Arsenal' }, + { name: 'M Tel-Aviv', country: 'Israel', id: 'Israel:M Tel-Aviv' }, + ], + [ + { name: 'Leverkusen', country: 'Germany', id: 'Germany:Leverkusen' }, + { name: 'Feyenoord', country: 'Netherlands', id: 'Netherlands:Feyenoord' }, + ], + [ + { name: 'Rangers', country: 'Scotland', id: 'Scotland:Rangers' }, + { name: 'Sturm', country: 'Austria', id: 'Austria:Sturm' }, + ], + [ + { name: 'Feyenoord', country: 'Netherlands', id: 'Netherlands:Feyenoord' }, + { name: 'Bologna', country: 'Italy', id: 'Italy:Bologna' }, + ], + [ + { name: 'Brest', country: 'France', id: 'France:Brest' }, + { name: 'Ferencváros', country: 'Hungary', id: 'Hungary:Ferencváros' }, + ], + [ + { name: 'Rangers', country: 'Scotland', id: 'Scotland:Rangers' }, + { name: 'Internazionale', country: 'Italy', id: 'Italy:Internazionale' }, + ], + [ + { name: 'Internazionale', country: 'Italy', id: 'Italy:Internazionale' }, + { name: 'M Tel-Aviv', country: 'Israel', id: 'Israel:M Tel-Aviv' }, + ], + [ + { name: 'Paris', country: 'France', id: 'France:Paris' }, + { name: 'Sporting CP', country: 'Portugal', id: 'Portugal:Sporting CP' }, + ], + [ + { name: 'Dinamo Zagreb', country: 'Croatia', id: 'Croatia:Dinamo Zagreb' }, + { name: 'Atlético', country: 'Spain', id: 'Spain:Atlético' }, + ], + [ + { name: 'Liverpool', country: 'England', id: 'England:Liverpool' }, + { name: 'Crvena zvezda', country: 'Serbia', id: 'Serbia:Crvena zvezda' }, + ], + [ + { name: 'Real Madrid', country: 'Spain', id: 'Spain:Real Madrid' }, + { name: 'Man City', country: 'England', id: 'England:Man City' }, + ], + [ + { name: 'Aston Villa', country: 'England', id: 'England:Aston Villa' }, + { name: 'Feyenoord', country: 'Netherlands', id: 'Netherlands:Feyenoord' }, + ], + [ + { name: 'Stuttgart', country: 'Germany', id: 'Germany:Stuttgart' }, + { name: 'Crvena zvezda', country: 'Serbia', id: 'Serbia:Crvena zvezda' }, + ], + [ + { name: 'Benfica', country: 'Portugal', id: 'Portugal:Benfica' }, + { name: 'Milan', country: 'Italy', id: 'Italy:Milan' }, + ], + [ + { name: 'Man City', country: 'England', id: 'England:Man City' }, + { name: 'Brest', country: 'France', id: 'France:Brest' }, + ], + [ + { name: 'Dinamo Zagreb', country: 'Croatia', id: 'Croatia:Dinamo Zagreb' }, + { name: 'Milan', country: 'Italy', id: 'Italy:Milan' }, + ], + [ + { name: 'Bayern', country: 'Germany', id: 'Germany:Bayern' }, + { name: 'Atlético', country: 'Spain', id: 'Spain:Atlético' }, + ], + [ + { name: 'Atalanta', country: 'Italy', id: 'Italy:Atalanta' }, + { name: 'Monaco', country: 'France', id: 'France:Monaco' }, + ], + [ + { name: 'Aston Villa', country: 'England', id: 'England:Aston Villa' }, + { name: 'Celtic', country: 'Scotland', id: 'Scotland:Celtic' }, + ], + [ + { name: 'Bayern', country: 'Germany', id: 'Germany:Bayern' }, + { name: 'Sturm', country: 'Austria', id: 'Austria:Sturm' }, + ], + [ + { name: 'Girona', country: 'Spain', id: 'Spain:Girona' }, + { name: 'Paris', country: 'France', id: 'France:Paris' }, + ], + [ + { name: 'Atalanta', country: 'Italy', id: 'Italy:Atalanta' }, + { name: 'PSV', country: 'Netherlands', id: 'Netherlands:PSV' }, + ], + [ + { name: 'Sturm', country: 'Austria', id: 'Austria:Sturm' }, + { + name: 'Slavia Praha', + country: 'Czech Republic', + id: 'Czech Republic:Slavia Praha', + }, + ], + [ + { name: 'PSV', country: 'Netherlands', id: 'Netherlands:PSV' }, + { name: 'Dortmund', country: 'Germany', id: 'Germany:Dortmund' }, + ], + [ + { name: 'Liverpool', country: 'England', id: 'England:Liverpool' }, + { name: 'Atalanta', country: 'Italy', id: 'Italy:Atalanta' }, + ], + [ + { name: 'Dortmund', country: 'Germany', id: 'Germany:Dortmund' }, + { name: 'Celtic', country: 'Scotland', id: 'Scotland:Celtic' }, + ], + [ + { name: 'Brest', country: 'France', id: 'France:Brest' }, + { name: 'Atalanta', country: 'Italy', id: 'Italy:Atalanta' }, + ], + [ + { name: 'Barcelona', country: 'Spain', id: 'Spain:Barcelona' }, + { name: 'Dortmund', country: 'Germany', id: 'Germany:Dortmund' }, + ], + [ + { name: 'Milan', country: 'Italy', id: 'Italy:Milan' }, + { name: 'PAOK', country: 'Greece', id: 'Greece:PAOK' }, + ], + [ + { name: 'Bologna', country: 'Italy', id: 'Italy:Bologna' }, + { name: 'Barcelona', country: 'Spain', id: 'Spain:Barcelona' }, + ], + [ + { name: 'Real Madrid', country: 'Spain', id: 'Spain:Real Madrid' }, + { name: 'Arsenal', country: 'England', id: 'England:Arsenal' }, + ], + [ + { name: 'Man City', country: 'England', id: 'England:Man City' }, + { name: 'Dinamo Zagreb', country: 'Croatia', id: 'Croatia:Dinamo Zagreb' }, + ], + [ + { name: 'Leipzig', country: 'Germany', id: 'Germany:Leipzig' }, + { name: 'Barcelona', country: 'Spain', id: 'Spain:Barcelona' }, + ], + [ + { name: 'Bologna', country: 'Italy', id: 'Italy:Bologna' }, + { name: 'Stuttgart', country: 'Germany', id: 'Germany:Stuttgart' }, + ], + [ + { name: 'Real Madrid', country: 'Spain', id: 'Spain:Real Madrid' }, + { name: 'Stuttgart', country: 'Germany', id: 'Germany:Stuttgart' }, + ], + [ + { name: 'Aston Villa', country: 'England', id: 'England:Aston Villa' }, + { name: 'Benfica', country: 'Portugal', id: 'Portugal:Benfica' }, + ], + [ + { name: 'Club Brugge', country: 'Belgium', id: 'Belgium:Club Brugge' }, + { name: 'PAOK', country: 'Greece', id: 'Greece:PAOK' }, + ], + [ + { name: 'Sporting CP', country: 'Portugal', id: 'Portugal:Sporting CP' }, + { name: 'Girona', country: 'Spain', id: 'Spain:Girona' }, + ], + [ + { name: 'M Tel-Aviv', country: 'Israel', id: 'Israel:M Tel-Aviv' }, + { name: 'Celtic', country: 'Scotland', id: 'Scotland:Celtic' }, + ], + [ + { name: 'Atalanta', country: 'Italy', id: 'Italy:Atalanta' }, + { name: 'Real Madrid', country: 'Spain', id: 'Spain:Real Madrid' }, + ], + [ + { name: 'Stuttgart', country: 'Germany', id: 'Germany:Stuttgart' }, + { name: 'Man City', country: 'England', id: 'England:Man City' }, + ], + [ + { name: 'Man City', country: 'England', id: 'England:Man City' }, + { name: 'Bayern', country: 'Germany', id: 'Germany:Bayern' }, + ], + [ + { name: 'Juventus', country: 'Italy', id: 'Italy:Juventus' }, + { name: 'Atlético', country: 'Spain', id: 'Spain:Atlético' }, + ], + [ + { name: 'Dortmund', country: 'Germany', id: 'Germany:Dortmund' }, + { name: 'Real Madrid', country: 'Spain', id: 'Spain:Real Madrid' }, + ], + [ + { name: 'Arsenal', country: 'England', id: 'England:Arsenal' }, + { name: 'Brest', country: 'France', id: 'France:Brest' }, + ], + [ + { name: 'Stuttgart', country: 'Germany', id: 'Germany:Stuttgart' }, + { name: 'Shakhtar', country: 'Ukraine', id: 'Ukraine:Shakhtar' }, + ], + [ + { name: 'Feyenoord', country: 'Netherlands', id: 'Netherlands:Feyenoord' }, + { name: 'Crvena zvezda', country: 'Serbia', id: 'Serbia:Crvena zvezda' }, + ], + [ + { name: 'Paris', country: 'France', id: 'France:Paris' }, + { name: 'Club Brugge', country: 'Belgium', id: 'Belgium:Club Brugge' }, + ], + [ + { name: 'Atlético', country: 'Spain', id: 'Spain:Atlético' }, + { name: 'Liverpool', country: 'England', id: 'England:Liverpool' }, + ], + [ + { + name: 'Slavia Praha', + country: 'Czech Republic', + id: 'Czech Republic:Slavia Praha', + }, + { name: 'Internazionale', country: 'Italy', id: 'Italy:Internazionale' }, + ], + [ + { name: 'PAOK', country: 'Greece', id: 'Greece:PAOK' }, + { name: 'Feyenoord', country: 'Netherlands', id: 'Netherlands:Feyenoord' }, + ], + [ + { name: 'Aston Villa', country: 'England', id: 'England:Aston Villa' }, + { name: 'Dortmund', country: 'Germany', id: 'Germany:Dortmund' }, + ], + [ + { name: 'PAOK', country: 'Greece', id: 'Greece:PAOK' }, + { name: 'Ferencváros', country: 'Hungary', id: 'Hungary:Ferencváros' }, + ], + [ + { name: 'Arsenal', country: 'England', id: 'England:Arsenal' }, + { name: 'Barcelona', country: 'Spain', id: 'Spain:Barcelona' }, + ], + [ + { name: 'Leverkusen', country: 'Germany', id: 'Germany:Leverkusen' }, + { name: 'Bologna', country: 'Italy', id: 'Italy:Bologna' }, + ], + [ + { name: 'Monaco', country: 'France', id: 'France:Monaco' }, + { name: 'Bologna', country: 'Italy', id: 'Italy:Bologna' }, + ], + [ + { name: 'Milan', country: 'Italy', id: 'Italy:Milan' }, + { name: 'Club Brugge', country: 'Belgium', id: 'Belgium:Club Brugge' }, + ], + [ + { name: 'Atlético', country: 'Spain', id: 'Spain:Atlético' }, + { name: 'Arsenal', country: 'England', id: 'England:Arsenal' }, + ], + [ + { name: 'Milan', country: 'Italy', id: 'Italy:Milan' }, + { name: 'Real Madrid', country: 'Spain', id: 'Spain:Real Madrid' }, + ], + [ + { name: 'Barcelona', country: 'Spain', id: 'Spain:Barcelona' }, + { name: 'Monaco', country: 'France', id: 'France:Monaco' }, + ], +]; diff --git a/src/pages/league/index.tsx b/src/pages/league/index.tsx new file mode 100644 index 00000000..81440fea --- /dev/null +++ b/src/pages/league/index.tsx @@ -0,0 +1,286 @@ +import { memo, useCallback, useEffect, useMemo, useState } from 'react'; +import styled, { css, keyframes } from 'styled-components'; + +import usePopup from '#store/usePopup'; +import rawPots from '#experiments/pots'; +import generatePairings from '#experiments/generatePairings'; +import generateSchedule from '#experiments/generateSchedule'; +import getCountryFlagUrl from '#utils/getCountryFlagUrl'; + +import games from './games'; + +const pots = rawPots.map(pot => + pot.map(team => ({ + ...team, + id: `${team.country}:${team.name}`, + })), +); + +type Team = (typeof pots)[number][number]; + +const Root = styled.div` + display: flex; + gap: 16px; + margin: 10px; +`; + +const Table = styled.table` + border-collapse: collapse; + border: 1px double rgb(128 128 128); + font-size: 10px; +`; + +const HeaderCell = styled.th<{ + hovered?: boolean; +}>` + vertical-align: bottom; + border: 1px solid rgb(192 192 192); + border-bottom-color: rgb(128 128 128); + padding: 3px 1px; + + &:nth-child(9n + 2) { + border-left: 1px double rgb(128 128 128); + } + + ${props => + props.hovered && + css` + background-color: rgba(0 0 0 / 0.1); + `} +`; + +const HeaderCellDiv = styled.div` + display: flex; + gap: 4px; + writing-mode: vertical-lr; + text-orientation: mixed; + font-weight: normal; + transform: rotate(180deg); + + > img { + width: 12px; + transform: rotate(90deg); + user-select: none; + pointer-events: none; + } +`; + +const BodyRow = styled.tr` + border: 1px solid rgb(192 192 192); + + &:hover { + background-color: rgba(0 0 0 / 0.1); + } + + &:nth-child(9n + 1) { + > td { + border-top: 1px double rgb(128 128 128); + } + } +`; + +const TeamCell = styled.td` + padding: 1px 3px; + border: 1px solid rgb(192 192 192); + + & + & { + text-align: center; + } + + &:nth-child(9n + 1) { + border-right: 1px double rgb(128 128 128); + } +`; + +const AppearLight = keyframes` + from { + background-color: rgb(255 255 0 / 0.5); + } +`; + +const TableCell = styled.td<{ + isMatch?: boolean; + hovered?: boolean; +}>` + border: 1px solid rgb(192 192 192); + + text-align: center; + + &:nth-child(9n + 1) { + border-right: 1px double rgb(128 128 128); + } + + ${props => + props.isMatch && + css` + animation: ${AppearLight} 3s ease-out normal forwards; + + &::before { + content: '✕'; + } + `} + + ${props => + props.hovered && + css` + background-color: rgba(0 0 0 / 0.1); + `} +`; + +const TeamDiv = styled.div<{ + country: string; +}>` + padding-left: 14.5px; + background-position-y: 1.5px; + background-size: 12px; + background-repeat: no-repeat; + + ${props => + props.country && + css` + background-image: url('${getCountryFlagUrl(props.country)}'); + `} +`; + +function LeagueStage() { + const [, setPopup] = usePopup(); + + const [hoverColumn, setHoverColumn] = useState(undefined); + + // @ts-expect-error Foo + const [pairings, setPairings] = useState<(readonly [Team, Team])[]>(games); + const [schedule, setSchedule] = useState<(readonly [Team, Team])[]>([]); + const [isFixturesDone, setIsFixturesDone] = useState(true); + + console.log('pairings', JSON.stringify(pairings)); + + const allTeams = useMemo(() => pots.flat(), []); + + useEffect(() => { + setPopup({ + waiting: false, + }); + }, []); + + // useEffect(() => { + // const foo = async () => { + // const generator = generatePairings({ + // pots, + // numMatchdays: 8, + // isMatchPossible: (a, b) => a.country !== b.country, + // }); + // for await (const pickedMatch of generator) { + // setPairings(prev => [...prev, pickedMatch]); + // } + // console.log('pairings', JSON.stringify(pairings)); + // setIsFixturesDone(true); + // }; + + // foo(); + // }, []); + + useEffect(() => { + if (isFixturesDone) { + const foo = async () => { + const generator = generateSchedule({ + matchdaySize: allTeams.length / 2, + matches: pairings, + }); + for await (const pickedMatch of generator) { + setSchedule(prev => [...prev, pickedMatch]); + } + }; + + setTimeout(() => { + // foo(); + }, 2000); + } + }, [isFixturesDone]); + + const pairingsMap = useMemo(() => { + const o: Record<`${string}:${string}`, boolean> = {}; + for (const pairing of pairings) { + o[`${pairing[0].name}:${pairing[1].name}`] = true; + } + return o; + }, [pairings]); + + const handleTableMouseOver = useCallback( + (e: React.MouseEvent) => { + const opponentId = + (e.target as HTMLTableCellElement).dataset.opponent || + ( + e.nativeEvent + .composedPath() + .find(el => (el as HTMLElement).dataset?.opponent) as + | HTMLElement + | undefined + )?.dataset?.opponent; + setHoverColumn(opponentId); + }, + [], + ); + + const handleTableMouseOut = useCallback( + (e: React.MouseEvent) => { + const opponentId = (e.target as HTMLTableCellElement).dataset.opponent; + if (opponentId) { + setHoverColumn(undefined); + } + }, + [], + ); + + return ( + + + + + + {allTeams.map(opponent => ( + + + {`[${opponent.country}]`} + {opponent.name} + + + ))} + + + + {allTeams.map(team => ( + + + {team.name} + + {allTeams.map(opponent => { + const isMatch = pairingsMap[`${team.name}:${opponent.name}`]; + return ( + + ); + })} + + ))} + +
+
Drawn matches: {pairings.length}/144
+
+ ); +} + +export default memo(LeagueStage); diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 5a0879c9..e9c3168e 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -15,6 +15,7 @@ import useDrawId from '#store/useDrawId'; import usePopup from '#store/usePopup'; import config from '../config'; +import League from '../pages/league'; import HeadMetadata from './HeadMetadata'; import Navbar from './Navbar'; @@ -95,6 +96,10 @@ function Routing() { ) : null} {/* TODO */} + } + /> Date: Sun, 30 Jun 2024 21:50:21 +0100 Subject: [PATCH 14/41] upd --- src/experiments/generateSchedule.ts | 227 ++----- .../getFirstSuitableMatch.wrapper.ts | 91 +-- src/experiments/getFirstSuitableMatchday.ts | 204 ++++++ .../getFirstSuitableMatchday.worker.ts | 7 + .../getFirstSuitableMatchday.wrapper.ts | 74 +++ src/experiments/index.ts | 22 - src/experiments/pots.ts | 2 +- src/experiments/raceWorkers.ts | 61 ++ src/pages/league/MatchesTable.tsx | 222 +++++++ src/pages/league/Schedule.tsx | 112 ++++ src/pages/league/games.ts | 610 ------------------ src/pages/league/index.tsx | 317 +++------ 12 files changed, 852 insertions(+), 1097 deletions(-) create mode 100644 src/experiments/getFirstSuitableMatchday.ts create mode 100644 src/experiments/getFirstSuitableMatchday.worker.ts create mode 100644 src/experiments/getFirstSuitableMatchday.wrapper.ts delete mode 100644 src/experiments/index.ts create mode 100644 src/experiments/raceWorkers.ts create mode 100644 src/pages/league/MatchesTable.tsx create mode 100644 src/pages/league/Schedule.tsx delete mode 100644 src/pages/league/games.ts diff --git a/src/experiments/generateSchedule.ts b/src/experiments/generateSchedule.ts index 79e1104e..47f6c569 100644 --- a/src/experiments/generateSchedule.ts +++ b/src/experiments/generateSchedule.ts @@ -1,184 +1,73 @@ -import { keyBy, orderBy } from 'lodash'; +import { keyBy, uniq } from 'lodash'; -import { findFirstSolution } from '#utils/backtrack'; +import getFirstSuitableMatchday from './getFirstSuitableMatchday.wrapper'; -const getFirstSuitableMatch = ({ +export default async function* generateSchedule({ matchdaySize, - remainingMatches: remainingMatchesUnordered, - schedule, + allGames: allGamesWithIds, + currentSchedule: foobar, }: { matchdaySize: number; - remainingMatches: readonly (readonly [number, number])[]; - schedule: readonly (readonly [number, number])[]; -}) => { - // team:md - const locationByMatchday: Record<`${number}:${number}`, boolean> = {}; - const numHomeGamesByTeam: Record = {}; - const numAwayGamesByTeam: Record = {}; - - const remainingMatches = orderBy(remainingMatchesUnordered, [ - m => m[0], - m => -m[1], - ]); - - debugger; - - for (const [i, [h, a]] of schedule.entries()) { - const matchdayIndex = Math.floor(i / matchdaySize); - locationByMatchday[`${h}:${matchdayIndex}`] = true; - locationByMatchday[`${a}:${matchdayIndex}`] = true; - numHomeGamesByTeam[h] = (numHomeGamesByTeam[h] ?? 0) + 1; - numAwayGamesByTeam[a] = (numAwayGamesByTeam[a] ?? 0) + 1; - } - - return remainingMatches.find(m => { - const solution = findFirstSolution( - { - source: remainingMatches, - target: schedule, - picked: m, - locationByMatchday, - numHomeGamesByTeam, - numAwayGamesByTeam, - }, - { - reject: c => { - const [h, a] = c.picked; - const currentMatchdayIndex = Math.floor( - c.target.length / matchdaySize, - ); - - const hasHomeTeamPlayedThisMatchday = - c.locationByMatchday[`${h}:${currentMatchdayIndex}`]; - if (hasHomeTeamPlayedThisMatchday) { - return true; - } - - const hasAwayTeamPlayedThisMatchday = - c.locationByMatchday[`${a}:${currentMatchdayIndex}`]; - if (hasAwayTeamPlayedThisMatchday) { - return true; - } - - if (c.numHomeGamesByTeam[h] > c.numAwayGamesByTeam[h]) { - return true; - } - - if (c.numAwayGamesByTeam[a] > c.numHomeGamesByTeam[a]) { - return true; - } - - return false; - }, - - accept: c => c.source.length === 1, - - generate: c => { - const oldMatchdayIndex = Math.floor(c.target.length / matchdaySize); - - const newTarget = [...c.target, c.picked]; - const newMatchdayIndex = Math.floor(newTarget.length / matchdaySize); - const newLocationByMatchday: typeof c.locationByMatchday = { - ...c.locationByMatchday, - [`${c.picked[0]}:${oldMatchdayIndex}`]: true, - [`${c.picked[1]}:${oldMatchdayIndex}`]: true, - } satisfies typeof c.locationByMatchday; - - const newNumHomeGamesByTeam = { - ...c.numHomeGamesByTeam, - [c.picked[0]]: (c.numHomeGamesByTeam[c.picked[0]] ?? 0) + 1, - } as typeof c.numHomeGamesByTeam; - const newNumAwayGamesByTeam = { - ...c.numAwayGamesByTeam, - [c.picked[1]]: (c.numAwayGamesByTeam[c.picked[1]] ?? 0) + 1, - } as typeof c.numAwayGamesByTeam; - - const newSource = c.source.filter(m => m !== c.picked); - - const thisMatchday = newTarget.slice(newMatchdayIndex * matchdaySize); - - const highestHomeThisMatchday = thisMatchday - // eslint-disable-next-line unicorn/no-array-reduce - .reduce((prev, cur) => Math.max(prev, cur[0]), -1); - - const foo = newSource.filter(([h, a]) => { - if (h <= highestHomeThisMatchday) { - return false; - } - - if (newLocationByMatchday[`${h}:${newMatchdayIndex}`]) { - return false; - } - - if (newLocationByMatchday[`${a}:${newMatchdayIndex}`]) { - return false; - } - - return true; - }); - - const uniqueHomeTeams = new Set(foo.map(m => m[0])); - - // console.log(newTarget.length); - - const candidates = []; - - const newMatchdaySize = newTarget.length % matchdaySize; - - if (newMatchdaySize + uniqueHomeTeams.size >= matchdaySize) { - for (const newPicked of foo) { - candidates.push({ - source: newSource, - target: newTarget, - picked: newPicked, - locationByMatchday: newLocationByMatchday, - numHomeGamesByTeam: newNumHomeGamesByTeam, - numAwayGamesByTeam: newNumAwayGamesByTeam, - }); - } - } - - return candidates; - }, - }, - ); + allGames: readonly (readonly [T, T])[]; + currentSchedule: readonly (readonly (readonly [T, T])[])[]; +}) { + const allTeams = allGamesWithIds.flat(); + const teamById = keyBy(allTeams, team => team.id); + const allTeamIds = uniq(allTeams.map(team => team.id)); + const indexByTeamId = new Map(allTeamIds.map((id, i) => [id, i] as const)); - if (!solution) { - console.log('sol', solution); + const currentSchedule: Record<`${number}:${number}`, number> = {}; + for (const [matchdayIndex, matchday] of foobar.entries()) { + for (const [h, a] of matchday) { + const homeIndex = indexByTeamId.get(h.id)!; + const awayIndex = indexByTeamId.get(a.id)!; + currentSchedule[`${homeIndex}:${awayIndex}`] = matchdayIndex; } - // console.log('sol', solution); - return solution !== undefined; - })!; -}; + } -export default function* generateSchedule({ - matchdaySize, - matches, -}: { - matchdaySize: number; - matches: readonly (readonly [T, T])[]; -}) { - const foo = matches.flat(); - const aaa = keyBy(foo, team => team.id); - const allTeamIds = [...new Set(foo.map(team => team.id))]; - const indexByTeamId = new Map(allTeamIds.map((id, i) => [id, i] as const)); + const allGamesUnordered: (readonly [number, number])[] = []; + for (const [h, a] of allGamesWithIds) { + const homeIndex = indexByTeamId.get(h.id)!; + const awayIndex = indexByTeamId.get(a.id)!; + allGamesUnordered.push([homeIndex, awayIndex]); + } - const schedule: (readonly [number, number])[] = []; - const remainingMatches = matches.map( - ([h, a]) => [indexByTeamId.get(h.id)!, indexByTeamId.get(a.id)!] as const, - ); + // const allGames = orderBy(allGamesUnordered, [ + // m => Math.min(...m), + // m => Math.max(...m), + // ]); - while (remainingMatches.length > 0) { - const pickedMatch = getFirstSuitableMatch({ + for (const [i, match] of allGamesUnordered.entries()) { + // eslint-disable-next-line no-await-in-loop + const result = await getFirstSuitableMatchday({ matchdaySize, - remainingMatches, - schedule, + allGames: allGamesUnordered, + currentSchedule, + matchIndex: i, }); - console.log('picked match schedule', pickedMatch); - schedule.push(pickedMatch); - remainingMatches.splice(remainingMatches.indexOf(pickedMatch), 1); + console.log('for match', match, 'picked', result.pickedMatchday); + currentSchedule[`${match[0]}:${match[1]}`] = result.pickedMatchday; + + const homeTeam = teamById[allTeamIds[match[0]]]; + const awayTeam = teamById[allTeamIds[match[1]]]; + const originalMatch = allGamesWithIds.find( + m => m[0].id === homeTeam.id && m[1].id === awayTeam.id, + )!; + + const haha = result.matchdays.map(md => + md.map(([h, a]) => { + const ht = teamById[allTeamIds[h]]; + const at = teamById[allTeamIds[a]]; + return allGamesWithIds.find( + mi => mi[0].id === ht.id && mi[1].id === at.id, + )!; + }), + ); - yield [aaa[allTeamIds[0]], aaa[allTeamIds[1]]] as const; + yield { + match: originalMatch, + matchday: result.pickedMatchday, + solutionSchedule: haha, + }; } - debugger; } diff --git a/src/experiments/getFirstSuitableMatch.wrapper.ts b/src/experiments/getFirstSuitableMatch.wrapper.ts index 9b0d93d6..1ec02734 100644 --- a/src/experiments/getFirstSuitableMatch.wrapper.ts +++ b/src/experiments/getFirstSuitableMatch.wrapper.ts @@ -1,76 +1,31 @@ -import delay from 'delay.js'; - -import workerSendAndReceive from '#utils/worker/sendAndReceive'; - import { type Func } from './getFirstSuitableMatch.worker'; +import raceWorkers from './raceWorkers'; const NUM_WORKERS = navigator.hardwareConcurrency - 1; -const fastest: Record = Object.fromEntries( - Array.from( - { - length: NUM_WORKERS, - }, - (_, i) => [i, 0] as const, - ), -); - export default async ( options: Omit[0], 'randomArray' | 'shouldShuffle'>, -) => { - const randomArray = Array.from( - { - length: options.allGames.length, - }, - () => Math.random(), - ); - - const firstResult = await new Promise<{ - index: number; - result: Awaited>; - }>(async resolve => { - const workers: Worker[] = []; - let isLoopRunning = true; - for (let i = 0; isLoopRunning; ++i) { - console.log('length', workers.length); - if (workers.length >= NUM_WORKERS) { - const oldestWorker = workers.shift(); - oldestWorker?.terminate(); - } - - const worker = new Worker( - new URL('./getFirstSuitableMatch.worker', import.meta.url), +) => + raceWorkers({ + numWorkers: NUM_WORKERS, + getWorker: () => + new Worker(new URL('./getFirstSuitableMatch.worker', import.meta.url)), + getPayload: (workerIndex, attempt) => { + const randomArray = Array.from( + { + length: options.allGames.length, + }, + () => Math.random(), ); - workers.push(worker); - - // eslint-disable-next-line no-loop-func - (async () => { - const result = await workerSendAndReceive< - Parameters[0], - ReturnType - >(worker)({ - ...options, - randomArray, - shouldShuffle: i > 0, - }); - isLoopRunning = false; - for (const w of workers) { - w.terminate(); - } - resolve({ - result, - index: i, - }); - })(); - - // eslint-disable-next-line no-await-in-loop - await delay(500 * Math.exp(i / 10)); - } + const shouldNotShuffle = workerIndex === 0 && attempt === 0; + return { + ...options, + randomArray, + shouldShuffle: !shouldNotShuffle, + }; + }, + getTimeout: (workerIndex, iteration) => { + const factor = 7 / (workerIndex + 1); + return factor * Math.min(10000, 1000 * Math.exp(iteration / 10)); + }, }); - - ++fastest[firstResult.index]; - - console.log('fastest worker', fastest); - - return firstResult.result; -}; diff --git a/src/experiments/getFirstSuitableMatchday.ts b/src/experiments/getFirstSuitableMatchday.ts new file mode 100644 index 00000000..4438fe9f --- /dev/null +++ b/src/experiments/getFirstSuitableMatchday.ts @@ -0,0 +1,204 @@ +import { range, shuffle } from 'lodash'; + +import { findFirstSolution } from '#utils/backtrack'; + +export default ({ + matchdaySize, + allGames, + currentSchedule, + matchIndex, +}: { + matchdaySize: number; + allGames: readonly (readonly [number, number])[]; + currentSchedule: Record<`${number}:${number}`, number>; + matchIndex: number; +}) => { + const numGames = allGames.length; + const numMatchdays = numGames / matchdaySize; + + let record = 0; + + const numMatchesPerMatchday = Array.from( + { + length: numMatchdays, + }, + () => 0, + ); + // team:md + const locationByMatchday: Record<`${number}:${number}`, 'h' | 'a'> = {}; + const numHomeGamesByTeam: Record = {}; + const numAwayGamesByTeam: Record = {}; + + for (const [key, matchdayIndex] of Object.entries(currentSchedule)) { + const [h, a] = key.split(':').map(Number); + ++numMatchesPerMatchday[matchdayIndex]; + locationByMatchday[`${h}:${matchdayIndex}`] = 'h'; + locationByMatchday[`${a}:${matchdayIndex}`] = 'a'; + numHomeGamesByTeam[h] = (numHomeGamesByTeam[h] ?? 0) + 1; + numAwayGamesByTeam[a] = (numAwayGamesByTeam[a] ?? 0) + 1; + } + + for (const pickedMatchday of shuffle(range(numMatchdays))) { + const solution = findFirstSolution( + { + matchIndex, + schedule: currentSchedule, + numMatchesPerMatchday, + pickedMatchday, + locationByMatchday, + numHomeGamesByTeam, + numAwayGamesByTeam, + }, + { + reject: c => { + const [h, a] = allGames[c.matchIndex]; + + // md is full + if (c.numMatchesPerMatchday[c.pickedMatchday] === matchdaySize) { + return true; + } + + // already played this md + const hasHomeTeamPlayedThisMatchday = + c.locationByMatchday[`${h}:${c.pickedMatchday}`]; + if (hasHomeTeamPlayedThisMatchday) { + return true; + } + + const hasAwayTeamPlayedThisMatchday = + c.locationByMatchday[`${a}:${c.pickedMatchday}`]; + if (hasAwayTeamPlayedThisMatchday) { + return true; + } + + for (let b = 0; b < 2; ++b) { + const loc = b === 0 ? 'h' : 'a'; + const t = b === 0 ? h : a; + + if (c.pickedMatchday <= 1) { + // is first two + if ( + c.locationByMatchday[`${t}:${1 - c.pickedMatchday}`] === loc + ) { + return true; + } + } else if (c.pickedMatchday >= numMatchdays - 2) { + // is last two + if ( + c.locationByMatchday[ + `${t}:${numMatchdays * 2 - 3 - c.pickedMatchday}` + ] === loc + ) { + return true; + } + } else { + const minus1 = + c.locationByMatchday[`${t}:${c.pickedMatchday - 1}`]; + const plus1 = + c.locationByMatchday[`${t}:${c.pickedMatchday + 1}`]; + if (minus1 === loc) { + if (plus1 === loc) { + return true; + } + const minus2 = + c.locationByMatchday[`${t}:${c.pickedMatchday - 2}`]; + if (minus2 === loc) { + return true; + } + } else { + const plus2 = + c.locationByMatchday[`${t}:${c.pickedMatchday + 2}`]; + if (plus1 === loc && plus2 === loc) { + return true; + } + } + } + } + + return false; + }, + + accept: c => c.matchIndex === numGames - 1, + + // eslint-disable-next-line no-loop-func + generate: c => { + const pickedMatch = allGames[c.matchIndex]; + + const newMatchIndex = c.matchIndex + 1; + + const newLocationByMatchday: typeof c.locationByMatchday = { + ...c.locationByMatchday, + [`${pickedMatch[0]}:${c.pickedMatchday}`]: 'h', + [`${pickedMatch[1]}:${c.pickedMatchday}`]: 'a', + } satisfies typeof c.locationByMatchday; + + const newNumHomeGamesByTeam = { + ...c.numHomeGamesByTeam, + [pickedMatch[0]]: (c.numHomeGamesByTeam[pickedMatch[0]] ?? 0) + 1, + } as typeof c.numHomeGamesByTeam; + const newNumAwayGamesByTeam = { + ...c.numAwayGamesByTeam, + [pickedMatch[1]]: (c.numAwayGamesByTeam[pickedMatch[1]] ?? 0) + 1, + } as typeof c.numAwayGamesByTeam; + + const newSchedule = { + ...c.schedule, + [`${pickedMatch[0]}:${pickedMatch[1]}`]: c.pickedMatchday, + } satisfies typeof c.schedule as typeof c.schedule; + + const newNumMatchesByMatchday = c.numMatchesPerMatchday.with( + c.pickedMatchday, + c.numMatchesPerMatchday[c.pickedMatchday] + 1, + ); + + // console.log(newMatchIndex); + + if (newMatchIndex > record) { + console.log(newMatchIndex); + record = newMatchIndex; + } + + // shuffling candidates makes it worse + const candidates: (typeof c)[] = []; + + for (let i = 0; i < numMatchdays; ++i) { + candidates.push({ + matchIndex: newMatchIndex, + schedule: newSchedule, + numMatchesPerMatchday: newNumMatchesByMatchday, + pickedMatchday: i, + locationByMatchday: newLocationByMatchday, + numHomeGamesByTeam: newNumHomeGamesByTeam, + numAwayGamesByTeam: newNumAwayGamesByTeam, + }); + } + + return shuffle(candidates); + }, + }, + ); + + // if (!solution) { + // console.log('sol', solution); + // } + if (solution) { + const arr = Array.from( + { + length: numMatchdays, + }, + () => [] as (readonly [number, number])[], + ); + for (const [key, matchdayIndex] of Object.entries(solution.schedule)) { + const m = key.split(':').map(Number) as [number, number]; + arr[matchdayIndex].push(m); + } + arr[solution.pickedMatchday].push(allGames[solution.matchIndex]); + return { + pickedMatchday, + matchdays: arr, + }; + } + } + + throw new Error('No solution!'); +}; diff --git a/src/experiments/getFirstSuitableMatchday.worker.ts b/src/experiments/getFirstSuitableMatchday.worker.ts new file mode 100644 index 00000000..181234c3 --- /dev/null +++ b/src/experiments/getFirstSuitableMatchday.worker.ts @@ -0,0 +1,7 @@ +import exposeWorker, { type ExposedFuncType } from '#utils/worker/expose'; + +import getFirstSuitableMatchday from './getFirstSuitableMatchday'; + +export type Func = ExposedFuncType; + +exposeWorker(getFirstSuitableMatchday); diff --git a/src/experiments/getFirstSuitableMatchday.wrapper.ts b/src/experiments/getFirstSuitableMatchday.wrapper.ts new file mode 100644 index 00000000..e243bf28 --- /dev/null +++ b/src/experiments/getFirstSuitableMatchday.wrapper.ts @@ -0,0 +1,74 @@ +import { remove, sample, shuffle, uniq } from 'lodash'; + +import raceWorkers from './raceWorkers'; +import { type Func } from './getFirstSuitableMatchday.worker'; + +const NUM_WORKERS = navigator.hardwareConcurrency - 1; + +export default ({ + matchdaySize, + allGames, + currentSchedule, + matchIndex, +}: { + matchdaySize: number; + allGames: readonly (readonly [number, number])[]; + currentSchedule: Record<`${number}:${number}`, number>; + matchIndex: number; +}) => + raceWorkers({ + numWorkers: NUM_WORKERS, + getWorker: () => + new Worker(new URL('./getFirstSuitableMatchday.worker', import.meta.url)), + getPayload: () => { + const allGamesShuffled = shuffle(allGames); + const allTeamsShuffled = uniq(allGames.flat()); + const matchesByTeam = Array.from( + { + length: allTeamsShuffled.length, + }, + () => 0, + ); + + for (const [h, a] of allGamesShuffled) { + ++matchesByTeam[h]; + ++matchesByTeam[a]; + } + + const res: typeof allGamesShuffled = []; + while (allGamesShuffled.length > 0) { + const min = Math.min(...matchesByTeam.filter(item => item > 0)); + const minIndices: number[] = []; + for (const [team, element] of matchesByTeam.entries()) { + if (element === min) { + minIndices.push(team); + } + } + const minTeam = sample(minIndices); + const minTeamMatches = remove( + allGamesShuffled, + m => m[0] === minTeam || m[1] === minTeam, + ); + for (const m of minTeamMatches) { + --matchesByTeam[m[0]]; + --matchesByTeam[m[1]]; + } + res.push(...minTeamMatches); + } + + // const indexByTeam = new Map( + // allTeamsShuffled.map((item, index) => [item, index]), + // ); + + return { + matchdaySize, + allGames: res, + currentSchedule, + matchIndex, + }; + }, + getTimeout: (workerIndex, attempt) => { + const factor = 7 / (workerIndex + 1) + return factor * Math.min(30000, 5000 * Math.exp(attempt / 10)); + }, + }); diff --git a/src/experiments/index.ts b/src/experiments/index.ts deleted file mode 100644 index 9d30fb3f..00000000 --- a/src/experiments/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import generatePairings from './generatePairings'; -import pots from './pots'; - -const NUM_MATCHDAYS = 8; - -type Team = (typeof pots)[number][number]; - -function canPlay(a: Team, b: Team) { - return a.country !== b.country; -} - -(async () => { - // const matchdays = await generatePairings({ - // pots, - // numMatchdays: NUM_MATCHDAYS, - // isMatchPossible: canPlay, - // }); - console.log( - 'final', - matchdays.map(m => [m[0].name, m[1].name]), - ); -})(); diff --git a/src/experiments/pots.ts b/src/experiments/pots.ts index e6b6e736..f59fd076 100644 --- a/src/experiments/pots.ts +++ b/src/experiments/pots.ts @@ -151,7 +151,7 @@ export default [ country: 'France', }, ], -] satisfies readonly (readonly { +] as const satisfies readonly (readonly { name: string; country: string; }[])[]; diff --git a/src/experiments/raceWorkers.ts b/src/experiments/raceWorkers.ts new file mode 100644 index 00000000..79055a73 --- /dev/null +++ b/src/experiments/raceWorkers.ts @@ -0,0 +1,61 @@ +import delay from 'delay.js'; + +import workerSendAndReceive from '#utils/worker/sendAndReceive'; + +export default async void>({ + numWorkers, + getWorker, + getPayload, + getTimeout, +}: { + numWorkers: number; + getWorker: () => Worker; + getPayload: (workerIndex: number, attempt: number) => Parameters[0]; + getTimeout: (workerIndex: number, attempt: number) => number; +}): Promise>> => { + const workers: Worker[] = []; + let gotResult = false; + const promises = Array.from( + { + length: numWorkers, + }, + async (_, workerIndex) => { + for (let attempt = 0; !gotResult; ++attempt) { + if (workerIndex === 0) { + console.log( + 'spawning', + workerIndex, + attempt, + getTimeout(workerIndex, attempt), + ); + } + const worker = getWorker(); + workers[workerIndex] = worker; + // eslint-disable-next-line no-await-in-loop + const raceResult = await Promise.race([ + workerSendAndReceive[0], ReturnType>(worker)( + getPayload(workerIndex, attempt), + ).catch(err => { + console.error(err); + }), + delay(getTimeout(workerIndex, attempt)), + ]); + if (raceResult !== undefined) { + gotResult = true; + for (const w of workers) { + w.terminate(); + } + return { + result: raceResult as Awaited>, + workerIndex, + attempt, + }; + } + // timed out + workers[workerIndex]?.terminate(); + } + }, + ); + const firstResult = (await Promise.race(promises))!; + return firstResult.result; +}; diff --git a/src/pages/league/MatchesTable.tsx b/src/pages/league/MatchesTable.tsx new file mode 100644 index 00000000..e9249cd0 --- /dev/null +++ b/src/pages/league/MatchesTable.tsx @@ -0,0 +1,222 @@ +import { memo, useCallback, useMemo, useState } from 'react'; +import styled, { css, keyframes } from 'styled-components'; + +import getCountryFlagUrl from '#utils/getCountryFlagUrl'; +import { type Country } from '#model/types'; + +const Table = styled.table` + border-collapse: collapse; + border: 1px double rgb(128 128 128); + font-size: 10px; +`; + +const HeaderCell = styled.th<{ + hovered?: boolean; +}>` + vertical-align: bottom; + border: 1px solid rgb(192 192 192); + border-bottom-color: rgb(128 128 128); + padding: 3px 1px; + + &:nth-child(9n + 2) { + border-left: 1px double rgb(128 128 128); + } + + ${props => + props.hovered && + css` + background-color: rgba(0 0 0 / 0.1); + `} +`; + +const HeaderCellDiv = styled.div` + display: flex; + gap: 4px; + writing-mode: vertical-lr; + text-orientation: mixed; + font-weight: normal; + transform: rotate(180deg); + + > img { + width: 12px; + transform: rotate(90deg); + user-select: none; + pointer-events: none; + } +`; + +const BodyRow = styled.tr` + border: 1px solid rgb(192 192 192); + + &:hover { + background-color: rgba(0 0 0 / 0.1); + } + + &:nth-child(9n + 1) { + > td { + border-top: 1px double rgb(128 128 128); + } + } +`; + +const TeamCell = styled.td` + padding: 1px 3px; + border: 1px solid rgb(192 192 192); + + & + & { + text-align: center; + } + + &:nth-child(9n + 1) { + border-right: 1px double rgb(128 128 128); + } +`; + +const AppearLight = keyframes` + from { + background-color: rgb(255 255 0 / 0.5); + } +`; + +const TableCell = styled.td<{ + isMatch?: boolean; + hovered?: boolean; +}>` + border: 1px solid rgb(192 192 192); + + text-align: center; + + &:nth-child(9n + 1) { + border-right: 1px double rgb(128 128 128); + } + + ${props => + props.isMatch && + css` + animation: ${AppearLight} 3s ease-out normal forwards; + + &::before { + content: '✕'; + } + `} + + ${props => + props.hovered && + css` + background-color: rgba(0 0 0 / 0.1); + `} +`; + +const TeamDiv = styled.div<{ + country: Country; +}>` + padding-left: 14.5px; + background-position-y: 1.5px; + background-size: 12px; + background-repeat: no-repeat; + + ${props => + props.country && + css` + background-image: url('${getCountryFlagUrl(props.country)}'); + `} +`; + +interface Team { + id: string; + name: string; + country: Country; +} + +interface Props { + allTeams: readonly Team[]; + pairings: (readonly [Team, Team])[]; +} + +function MatchesTable({ allTeams, pairings }: Props) { + const [hoverColumn, setHoverColumn] = useState(undefined); + + const pairingsMap = useMemo(() => { + const o: Record<`${string}:${string}`, boolean> = {}; + for (const pairing of pairings) { + o[`${pairing[0].name}:${pairing[1].name}`] = true; + } + return o; + }, [pairings]); + + const handleTableMouseOver = useCallback( + (e: React.MouseEvent) => { + const opponentId = + (e.target as HTMLTableCellElement).dataset.opponent || + ( + e.nativeEvent + .composedPath() + .find(el => (el as HTMLElement).dataset?.opponent) as + | HTMLElement + | undefined + )?.dataset?.opponent; + setHoverColumn(opponentId); + }, + [], + ); + + const handleTableMouseOut = useCallback( + (e: React.MouseEvent) => { + const opponentId = (e.target as HTMLTableCellElement).dataset.opponent; + if (opponentId) { + setHoverColumn(undefined); + } + }, + [], + ); + + return ( + + + + + {allTeams.map(opponent => ( + + + {`[${opponent.country}]`} + {opponent.name} + + + ))} + + + + {allTeams.map(team => ( + + + {team.name} + + {allTeams.map(opponent => { + const isMatch = pairingsMap[`${team.name}:${opponent.name}`]; + return ( + + ); + })} + + ))} + +
+ ); +} + +export default memo(MatchesTable); diff --git a/src/pages/league/Schedule.tsx b/src/pages/league/Schedule.tsx new file mode 100644 index 00000000..79228852 --- /dev/null +++ b/src/pages/league/Schedule.tsx @@ -0,0 +1,112 @@ +import { memo, useLayoutEffect } from 'react'; +import styled from 'styled-components'; + +import ContentWithFlag from '#ui/table/ContentWithFlag'; +import { type Country } from '#model/types'; + +const Root = styled.div` + width: 100%; + container-type: inline-size; +`; + +const CalendarContainer = styled.div` + display: grid; + gap: 16px; + grid-template-columns: repeat(4, 1fr); + width: fit-content; + + @container (max-width: 1000px) { + grid-template-columns: repeat(3, 1fr); + } + + @container (max-width: 500px) { + grid-template-columns: repeat(2, 1fr); + } +`; + +const MatchdayRoot = styled.div` + border: 1px double rgb(128 128 128); + font-size: 12px; +`; + +const MatchdayHeader = styled.div` + display: flex; + justify-content: center; + align-items: center; + height: 20px; +`; + +const MatchPair = styled.div` + display: flex; + align-items: center; + border-top: 1px solid rgb(192 192 192); + height: 20px; + + > :first-child { + align-items: end; + } +`; + +const MatchPairCenter = styled.div` + display: flex; + justify-content: center; + width: 20px; +`; + +const ScheduleTeamWrapper = styled.div` + /* width: 300px; */ + /* padding: 1px 3px; */ +`; + +interface Team { + name: string; + country: Country; +} + +interface Props { + schedule: readonly (readonly (readonly [Team, Team])[])[]; +} + +function Schedule({ schedule }: Props) { + useLayoutEffect(() => { + if (schedule.some(md => md.length > 0)) { + const elements = document.querySelectorAll('.team-div'); + const offsetWidths = [...elements].map( + el => (el as HTMLElement).offsetWidth ?? 0, + ); + const maxOffsetWidth = Math.max(...offsetWidths); + for (const element of elements) { + (element as HTMLElement).style.width = `${maxOffsetWidth}px`; + } + } + }, [schedule]); + + return ( + + + {schedule.map((md, i) => ( + + MATCHDAY {i + 1} + {md.map(m => ( + + + + {m[0].name} + + + - + + + {m[1].name} + + + + ))} + + ))} + + + ); +} + +export default memo(Schedule); diff --git a/src/pages/league/games.ts b/src/pages/league/games.ts deleted file mode 100644 index b648fd77..00000000 --- a/src/pages/league/games.ts +++ /dev/null @@ -1,610 +0,0 @@ -export default [ - [ - { name: 'Internazionale', country: 'Italy', id: 'Italy:Internazionale' }, - { name: 'Paris', country: 'France', id: 'France:Paris' }, - ], - [ - { name: 'Real Madrid', country: 'Spain', id: 'Spain:Real Madrid' }, - { - name: 'Slavia Praha', - country: 'Czech Republic', - id: 'Czech Republic:Slavia Praha', - }, - ], - [ - { name: 'Bayern', country: 'Germany', id: 'Germany:Bayern' }, - { name: 'PSV', country: 'Netherlands', id: 'Netherlands:PSV' }, - ], - [ - { name: 'Dinamo Zagreb', country: 'Croatia', id: 'Croatia:Dinamo Zagreb' }, - { name: 'Sturm', country: 'Austria', id: 'Austria:Sturm' }, - ], - [ - { name: 'Shakhtar', country: 'Ukraine', id: 'Ukraine:Shakhtar' }, - { name: 'Girona', country: 'Spain', id: 'Spain:Girona' }, - ], - [ - { name: 'PSV', country: 'Netherlands', id: 'Netherlands:PSV' }, - { name: 'Monaco', country: 'France', id: 'France:Monaco' }, - ], - [ - { name: 'Benfica', country: 'Portugal', id: 'Portugal:Benfica' }, - { name: 'Shakhtar', country: 'Ukraine', id: 'Ukraine:Shakhtar' }, - ], - [ - { name: 'Sporting CP', country: 'Portugal', id: 'Portugal:Sporting CP' }, - { name: 'Barcelona', country: 'Spain', id: 'Spain:Barcelona' }, - ], - [ - { name: 'Brest', country: 'France', id: 'France:Brest' }, - { name: 'Milan', country: 'Italy', id: 'Italy:Milan' }, - ], - [ - { name: 'Paris', country: 'France', id: 'France:Paris' }, - { name: 'Ferencváros', country: 'Hungary', id: 'Hungary:Ferencváros' }, - ], - [ - { name: 'PAOK', country: 'Greece', id: 'Greece:PAOK' }, - { name: 'Man City', country: 'England', id: 'England:Man City' }, - ], - [ - { name: 'Ferencváros', country: 'Hungary', id: 'Hungary:Ferencváros' }, - { name: 'Sturm', country: 'Austria', id: 'Austria:Sturm' }, - ], - [ - { name: 'Liverpool', country: 'England', id: 'England:Liverpool' }, - { name: 'Bologna', country: 'Italy', id: 'Italy:Bologna' }, - ], - [ - { name: 'Rangers', country: 'Scotland', id: 'Scotland:Rangers' }, - { name: 'Leverkusen', country: 'Germany', id: 'Germany:Leverkusen' }, - ], - [ - { name: 'Paris', country: 'France', id: 'France:Paris' }, - { name: 'Leipzig', country: 'Germany', id: 'Germany:Leipzig' }, - ], - [ - { name: 'Atalanta', country: 'Italy', id: 'Italy:Atalanta' }, - { name: 'Club Brugge', country: 'Belgium', id: 'Belgium:Club Brugge' }, - ], - [ - { name: 'Leipzig', country: 'Germany', id: 'Germany:Leipzig' }, - { name: 'Aston Villa', country: 'England', id: 'England:Aston Villa' }, - ], - [ - { name: 'Leipzig', country: 'Germany', id: 'Germany:Leipzig' }, - { name: 'PAOK', country: 'Greece', id: 'Greece:PAOK' }, - ], - [ - { name: 'Shakhtar', country: 'Ukraine', id: 'Ukraine:Shakhtar' }, - { name: 'Sporting CP', country: 'Portugal', id: 'Portugal:Sporting CP' }, - ], - [ - { name: 'Stuttgart', country: 'Germany', id: 'Germany:Stuttgart' }, - { name: 'Girona', country: 'Spain', id: 'Spain:Girona' }, - ], - [ - { name: 'Leverkusen', country: 'Germany', id: 'Germany:Leverkusen' }, - { name: 'Benfica', country: 'Portugal', id: 'Portugal:Benfica' }, - ], - [ - { - name: 'Slavia Praha', - country: 'Czech Republic', - id: 'Czech Republic:Slavia Praha', - }, - { name: 'Dinamo Zagreb', country: 'Croatia', id: 'Croatia:Dinamo Zagreb' }, - ], - [ - { name: 'M Tel-Aviv', country: 'Israel', id: 'Israel:M Tel-Aviv' }, - { name: 'Shakhtar', country: 'Ukraine', id: 'Ukraine:Shakhtar' }, - ], - [ - { name: 'Bologna', country: 'Italy', id: 'Italy:Bologna' }, - { name: 'Arsenal', country: 'England', id: 'England:Arsenal' }, - ], - [ - { name: 'Crvena zvezda', country: 'Serbia', id: 'Serbia:Crvena zvezda' }, - { name: 'Leipzig', country: 'Germany', id: 'Germany:Leipzig' }, - ], - [ - { name: 'Barcelona', country: 'Spain', id: 'Spain:Barcelona' }, - { name: 'Leverkusen', country: 'Germany', id: 'Germany:Leverkusen' }, - ], - [ - { name: 'Sporting CP', country: 'Portugal', id: 'Portugal:Sporting CP' }, - { name: 'Leverkusen', country: 'Germany', id: 'Germany:Leverkusen' }, - ], - [ - { name: 'Crvena zvezda', country: 'Serbia', id: 'Serbia:Crvena zvezda' }, - { name: 'PSV', country: 'Netherlands', id: 'Netherlands:PSV' }, - ], - [ - { name: 'Leverkusen', country: 'Germany', id: 'Germany:Leverkusen' }, - { name: 'Man City', country: 'England', id: 'England:Man City' }, - ], - [ - { name: 'Crvena zvezda', country: 'Serbia', id: 'Serbia:Crvena zvezda' }, - { name: 'Brest', country: 'France', id: 'France:Brest' }, - ], - [ - { name: 'Shakhtar', country: 'Ukraine', id: 'Ukraine:Shakhtar' }, - { name: 'Leipzig', country: 'Germany', id: 'Germany:Leipzig' }, - ], - [ - { name: 'Shakhtar', country: 'Ukraine', id: 'Ukraine:Shakhtar' }, - { name: 'Rangers', country: 'Scotland', id: 'Scotland:Rangers' }, - ], - [ - { name: 'Bayern', country: 'Germany', id: 'Germany:Bayern' }, - { name: 'Liverpool', country: 'England', id: 'England:Liverpool' }, - ], - [ - { name: 'Brest', country: 'France', id: 'France:Brest' }, - { name: 'Leipzig', country: 'Germany', id: 'Germany:Leipzig' }, - ], - [ - { name: 'Dinamo Zagreb', country: 'Croatia', id: 'Croatia:Dinamo Zagreb' }, - { name: 'Liverpool', country: 'England', id: 'England:Liverpool' }, - ], - [ - { name: 'Club Brugge', country: 'Belgium', id: 'Belgium:Club Brugge' }, - { name: 'Stuttgart', country: 'Germany', id: 'Germany:Stuttgart' }, - ], - [ - { name: 'Internazionale', country: 'Italy', id: 'Italy:Internazionale' }, - { name: 'Girona', country: 'Spain', id: 'Spain:Girona' }, - ], - [ - { name: 'Girona', country: 'Spain', id: 'Spain:Girona' }, - { name: 'PAOK', country: 'Greece', id: 'Greece:PAOK' }, - ], - [ - { name: 'Juventus', country: 'Italy', id: 'Italy:Juventus' }, - { name: 'Celtic', country: 'Scotland', id: 'Scotland:Celtic' }, - ], - [ - { name: 'Man City', country: 'England', id: 'England:Man City' }, - { name: 'Shakhtar', country: 'Ukraine', id: 'Ukraine:Shakhtar' }, - ], - [ - { name: 'Benfica', country: 'Portugal', id: 'Portugal:Benfica' }, - { name: 'Dortmund', country: 'Germany', id: 'Germany:Dortmund' }, - ], - [ - { name: 'Monaco', country: 'France', id: 'France:Monaco' }, - { name: 'Liverpool', country: 'England', id: 'England:Liverpool' }, - ], - [ - { name: 'Sturm', country: 'Austria', id: 'Austria:Sturm' }, - { name: 'Monaco', country: 'France', id: 'France:Monaco' }, - ], - [ - { name: 'Ferencváros', country: 'Hungary', id: 'Hungary:Ferencváros' }, - { name: 'PSV', country: 'Netherlands', id: 'Netherlands:PSV' }, - ], - [ - { name: 'Sturm', country: 'Austria', id: 'Austria:Sturm' }, - { name: 'Leverkusen', country: 'Germany', id: 'Germany:Leverkusen' }, - ], - [ - { name: 'Atlético', country: 'Spain', id: 'Spain:Atlético' }, - { name: 'Crvena zvezda', country: 'Serbia', id: 'Serbia:Crvena zvezda' }, - ], - [ - { name: 'Rangers', country: 'Scotland', id: 'Scotland:Rangers' }, - { - name: 'Slavia Praha', - country: 'Czech Republic', - id: 'Czech Republic:Slavia Praha', - }, - ], - [ - { name: 'Juventus', country: 'Italy', id: 'Italy:Juventus' }, - { name: 'Dinamo Zagreb', country: 'Croatia', id: 'Croatia:Dinamo Zagreb' }, - ], - [ - { name: 'Feyenoord', country: 'Netherlands', id: 'Netherlands:Feyenoord' }, - { name: 'Benfica', country: 'Portugal', id: 'Portugal:Benfica' }, - ], - [ - { name: 'Bologna', country: 'Italy', id: 'Italy:Bologna' }, - { name: 'M Tel-Aviv', country: 'Israel', id: 'Israel:M Tel-Aviv' }, - ], - [ - { name: 'Arsenal', country: 'England', id: 'England:Arsenal' }, - { name: 'Atalanta', country: 'Italy', id: 'Italy:Atalanta' }, - ], - [ - { name: 'Dortmund', country: 'Germany', id: 'Germany:Dortmund' }, - { name: 'Juventus', country: 'Italy', id: 'Italy:Juventus' }, - ], - [ - { name: 'Club Brugge', country: 'Belgium', id: 'Belgium:Club Brugge' }, - { name: 'Juventus', country: 'Italy', id: 'Italy:Juventus' }, - ], - [ - { name: 'PSV', country: 'Netherlands', id: 'Netherlands:PSV' }, - { name: 'Sporting CP', country: 'Portugal', id: 'Portugal:Sporting CP' }, - ], - [ - { name: 'Monaco', country: 'France', id: 'France:Monaco' }, - { name: 'Atlético', country: 'Spain', id: 'Spain:Atlético' }, - ], - [ - { name: 'Celtic', country: 'Scotland', id: 'Scotland:Celtic' }, - { name: 'Club Brugge', country: 'Belgium', id: 'Belgium:Club Brugge' }, - ], - [ - { name: 'Monaco', country: 'France', id: 'France:Monaco' }, - { name: 'Dinamo Zagreb', country: 'Croatia', id: 'Croatia:Dinamo Zagreb' }, - ], - [ - { name: 'Juventus', country: 'Italy', id: 'Italy:Juventus' }, - { name: 'Paris', country: 'France', id: 'France:Paris' }, - ], - [ - { name: 'Girona', country: 'Spain', id: 'Spain:Girona' }, - { name: 'Juventus', country: 'Italy', id: 'Italy:Juventus' }, - ], - [ - { name: 'Sturm', country: 'Austria', id: 'Austria:Sturm' }, - { name: 'Internazionale', country: 'Italy', id: 'Italy:Internazionale' }, - ], - [ - { name: 'Girona', country: 'Spain', id: 'Spain:Girona' }, - { name: 'Aston Villa', country: 'England', id: 'England:Aston Villa' }, - ], - [ - { name: 'Barcelona', country: 'Spain', id: 'Spain:Barcelona' }, - { name: 'Milan', country: 'Italy', id: 'Italy:Milan' }, - ], - [ - { name: 'Celtic', country: 'Scotland', id: 'Scotland:Celtic' }, - { name: 'Bayern', country: 'Germany', id: 'Germany:Bayern' }, - ], - [ - { name: 'Benfica', country: 'Portugal', id: 'Portugal:Benfica' }, - { name: 'Ferencváros', country: 'Hungary', id: 'Hungary:Ferencváros' }, - ], - [ - { name: 'Atlético', country: 'Spain', id: 'Spain:Atlético' }, - { name: 'Aston Villa', country: 'England', id: 'England:Aston Villa' }, - ], - [ - { name: 'Sporting CP', country: 'Portugal', id: 'Portugal:Sporting CP' }, - { name: 'M Tel-Aviv', country: 'Israel', id: 'Israel:M Tel-Aviv' }, - ], - [ - { name: 'Crvena zvezda', country: 'Serbia', id: 'Serbia:Crvena zvezda' }, - { name: 'Rangers', country: 'Scotland', id: 'Scotland:Rangers' }, - ], - [ - { name: 'PSV', country: 'Netherlands', id: 'Netherlands:PSV' }, - { name: 'Arsenal', country: 'England', id: 'England:Arsenal' }, - ], - [ - { name: 'Liverpool', country: 'England', id: 'England:Liverpool' }, - { name: 'Internazionale', country: 'Italy', id: 'Italy:Internazionale' }, - ], - [ - { name: 'Milan', country: 'Italy', id: 'Italy:Milan' }, - { name: 'Aston Villa', country: 'England', id: 'England:Aston Villa' }, - ], - [ - { name: 'Ferencváros', country: 'Hungary', id: 'Hungary:Ferencváros' }, - { name: 'Real Madrid', country: 'Spain', id: 'Spain:Real Madrid' }, - ], - [ - { name: 'Dortmund', country: 'Germany', id: 'Germany:Dortmund' }, - { name: 'Feyenoord', country: 'Netherlands', id: 'Netherlands:Feyenoord' }, - ], - [ - { name: 'Celtic', country: 'Scotland', id: 'Scotland:Celtic' }, - { name: 'Sporting CP', country: 'Portugal', id: 'Portugal:Sporting CP' }, - ], - [ - { name: 'Ferencváros', country: 'Hungary', id: 'Hungary:Ferencváros' }, - { name: 'Rangers', country: 'Scotland', id: 'Scotland:Rangers' }, - ], - [ - { name: 'Internazionale', country: 'Italy', id: 'Italy:Internazionale' }, - { name: 'Benfica', country: 'Portugal', id: 'Portugal:Benfica' }, - ], - [ - { - name: 'Slavia Praha', - country: 'Czech Republic', - id: 'Czech Republic:Slavia Praha', - }, - { name: 'Atalanta', country: 'Italy', id: 'Italy:Atalanta' }, - ], - [ - { name: 'M Tel-Aviv', country: 'Israel', id: 'Israel:M Tel-Aviv' }, - { - name: 'Slavia Praha', - country: 'Czech Republic', - id: 'Czech Republic:Slavia Praha', - }, - ], - [ - { name: 'Feyenoord', country: 'Netherlands', id: 'Netherlands:Feyenoord' }, - { name: 'Paris', country: 'France', id: 'France:Paris' }, - ], - [ - { name: 'Leipzig', country: 'Germany', id: 'Germany:Leipzig' }, - { name: 'Rangers', country: 'Scotland', id: 'Scotland:Rangers' }, - ], - [ - { name: 'PAOK', country: 'Greece', id: 'Greece:PAOK' }, - { name: 'Juventus', country: 'Italy', id: 'Italy:Juventus' }, - ], - [ - { name: 'M Tel-Aviv', country: 'Israel', id: 'Israel:M Tel-Aviv' }, - { name: 'Bayern', country: 'Germany', id: 'Germany:Bayern' }, - ], - [ - { - name: 'Slavia Praha', - country: 'Czech Republic', - id: 'Czech Republic:Slavia Praha', - }, - { name: 'Stuttgart', country: 'Germany', id: 'Germany:Stuttgart' }, - ], - [ - { name: 'Club Brugge', country: 'Belgium', id: 'Belgium:Club Brugge' }, - { name: 'Bayern', country: 'Germany', id: 'Germany:Bayern' }, - ], - [ - { name: 'Celtic', country: 'Scotland', id: 'Scotland:Celtic' }, - { name: 'Brest', country: 'France', id: 'France:Brest' }, - ], - [ - { name: 'Arsenal', country: 'England', id: 'England:Arsenal' }, - { name: 'M Tel-Aviv', country: 'Israel', id: 'Israel:M Tel-Aviv' }, - ], - [ - { name: 'Leverkusen', country: 'Germany', id: 'Germany:Leverkusen' }, - { name: 'Feyenoord', country: 'Netherlands', id: 'Netherlands:Feyenoord' }, - ], - [ - { name: 'Rangers', country: 'Scotland', id: 'Scotland:Rangers' }, - { name: 'Sturm', country: 'Austria', id: 'Austria:Sturm' }, - ], - [ - { name: 'Feyenoord', country: 'Netherlands', id: 'Netherlands:Feyenoord' }, - { name: 'Bologna', country: 'Italy', id: 'Italy:Bologna' }, - ], - [ - { name: 'Brest', country: 'France', id: 'France:Brest' }, - { name: 'Ferencváros', country: 'Hungary', id: 'Hungary:Ferencváros' }, - ], - [ - { name: 'Rangers', country: 'Scotland', id: 'Scotland:Rangers' }, - { name: 'Internazionale', country: 'Italy', id: 'Italy:Internazionale' }, - ], - [ - { name: 'Internazionale', country: 'Italy', id: 'Italy:Internazionale' }, - { name: 'M Tel-Aviv', country: 'Israel', id: 'Israel:M Tel-Aviv' }, - ], - [ - { name: 'Paris', country: 'France', id: 'France:Paris' }, - { name: 'Sporting CP', country: 'Portugal', id: 'Portugal:Sporting CP' }, - ], - [ - { name: 'Dinamo Zagreb', country: 'Croatia', id: 'Croatia:Dinamo Zagreb' }, - { name: 'Atlético', country: 'Spain', id: 'Spain:Atlético' }, - ], - [ - { name: 'Liverpool', country: 'England', id: 'England:Liverpool' }, - { name: 'Crvena zvezda', country: 'Serbia', id: 'Serbia:Crvena zvezda' }, - ], - [ - { name: 'Real Madrid', country: 'Spain', id: 'Spain:Real Madrid' }, - { name: 'Man City', country: 'England', id: 'England:Man City' }, - ], - [ - { name: 'Aston Villa', country: 'England', id: 'England:Aston Villa' }, - { name: 'Feyenoord', country: 'Netherlands', id: 'Netherlands:Feyenoord' }, - ], - [ - { name: 'Stuttgart', country: 'Germany', id: 'Germany:Stuttgart' }, - { name: 'Crvena zvezda', country: 'Serbia', id: 'Serbia:Crvena zvezda' }, - ], - [ - { name: 'Benfica', country: 'Portugal', id: 'Portugal:Benfica' }, - { name: 'Milan', country: 'Italy', id: 'Italy:Milan' }, - ], - [ - { name: 'Man City', country: 'England', id: 'England:Man City' }, - { name: 'Brest', country: 'France', id: 'France:Brest' }, - ], - [ - { name: 'Dinamo Zagreb', country: 'Croatia', id: 'Croatia:Dinamo Zagreb' }, - { name: 'Milan', country: 'Italy', id: 'Italy:Milan' }, - ], - [ - { name: 'Bayern', country: 'Germany', id: 'Germany:Bayern' }, - { name: 'Atlético', country: 'Spain', id: 'Spain:Atlético' }, - ], - [ - { name: 'Atalanta', country: 'Italy', id: 'Italy:Atalanta' }, - { name: 'Monaco', country: 'France', id: 'France:Monaco' }, - ], - [ - { name: 'Aston Villa', country: 'England', id: 'England:Aston Villa' }, - { name: 'Celtic', country: 'Scotland', id: 'Scotland:Celtic' }, - ], - [ - { name: 'Bayern', country: 'Germany', id: 'Germany:Bayern' }, - { name: 'Sturm', country: 'Austria', id: 'Austria:Sturm' }, - ], - [ - { name: 'Girona', country: 'Spain', id: 'Spain:Girona' }, - { name: 'Paris', country: 'France', id: 'France:Paris' }, - ], - [ - { name: 'Atalanta', country: 'Italy', id: 'Italy:Atalanta' }, - { name: 'PSV', country: 'Netherlands', id: 'Netherlands:PSV' }, - ], - [ - { name: 'Sturm', country: 'Austria', id: 'Austria:Sturm' }, - { - name: 'Slavia Praha', - country: 'Czech Republic', - id: 'Czech Republic:Slavia Praha', - }, - ], - [ - { name: 'PSV', country: 'Netherlands', id: 'Netherlands:PSV' }, - { name: 'Dortmund', country: 'Germany', id: 'Germany:Dortmund' }, - ], - [ - { name: 'Liverpool', country: 'England', id: 'England:Liverpool' }, - { name: 'Atalanta', country: 'Italy', id: 'Italy:Atalanta' }, - ], - [ - { name: 'Dortmund', country: 'Germany', id: 'Germany:Dortmund' }, - { name: 'Celtic', country: 'Scotland', id: 'Scotland:Celtic' }, - ], - [ - { name: 'Brest', country: 'France', id: 'France:Brest' }, - { name: 'Atalanta', country: 'Italy', id: 'Italy:Atalanta' }, - ], - [ - { name: 'Barcelona', country: 'Spain', id: 'Spain:Barcelona' }, - { name: 'Dortmund', country: 'Germany', id: 'Germany:Dortmund' }, - ], - [ - { name: 'Milan', country: 'Italy', id: 'Italy:Milan' }, - { name: 'PAOK', country: 'Greece', id: 'Greece:PAOK' }, - ], - [ - { name: 'Bologna', country: 'Italy', id: 'Italy:Bologna' }, - { name: 'Barcelona', country: 'Spain', id: 'Spain:Barcelona' }, - ], - [ - { name: 'Real Madrid', country: 'Spain', id: 'Spain:Real Madrid' }, - { name: 'Arsenal', country: 'England', id: 'England:Arsenal' }, - ], - [ - { name: 'Man City', country: 'England', id: 'England:Man City' }, - { name: 'Dinamo Zagreb', country: 'Croatia', id: 'Croatia:Dinamo Zagreb' }, - ], - [ - { name: 'Leipzig', country: 'Germany', id: 'Germany:Leipzig' }, - { name: 'Barcelona', country: 'Spain', id: 'Spain:Barcelona' }, - ], - [ - { name: 'Bologna', country: 'Italy', id: 'Italy:Bologna' }, - { name: 'Stuttgart', country: 'Germany', id: 'Germany:Stuttgart' }, - ], - [ - { name: 'Real Madrid', country: 'Spain', id: 'Spain:Real Madrid' }, - { name: 'Stuttgart', country: 'Germany', id: 'Germany:Stuttgart' }, - ], - [ - { name: 'Aston Villa', country: 'England', id: 'England:Aston Villa' }, - { name: 'Benfica', country: 'Portugal', id: 'Portugal:Benfica' }, - ], - [ - { name: 'Club Brugge', country: 'Belgium', id: 'Belgium:Club Brugge' }, - { name: 'PAOK', country: 'Greece', id: 'Greece:PAOK' }, - ], - [ - { name: 'Sporting CP', country: 'Portugal', id: 'Portugal:Sporting CP' }, - { name: 'Girona', country: 'Spain', id: 'Spain:Girona' }, - ], - [ - { name: 'M Tel-Aviv', country: 'Israel', id: 'Israel:M Tel-Aviv' }, - { name: 'Celtic', country: 'Scotland', id: 'Scotland:Celtic' }, - ], - [ - { name: 'Atalanta', country: 'Italy', id: 'Italy:Atalanta' }, - { name: 'Real Madrid', country: 'Spain', id: 'Spain:Real Madrid' }, - ], - [ - { name: 'Stuttgart', country: 'Germany', id: 'Germany:Stuttgart' }, - { name: 'Man City', country: 'England', id: 'England:Man City' }, - ], - [ - { name: 'Man City', country: 'England', id: 'England:Man City' }, - { name: 'Bayern', country: 'Germany', id: 'Germany:Bayern' }, - ], - [ - { name: 'Juventus', country: 'Italy', id: 'Italy:Juventus' }, - { name: 'Atlético', country: 'Spain', id: 'Spain:Atlético' }, - ], - [ - { name: 'Dortmund', country: 'Germany', id: 'Germany:Dortmund' }, - { name: 'Real Madrid', country: 'Spain', id: 'Spain:Real Madrid' }, - ], - [ - { name: 'Arsenal', country: 'England', id: 'England:Arsenal' }, - { name: 'Brest', country: 'France', id: 'France:Brest' }, - ], - [ - { name: 'Stuttgart', country: 'Germany', id: 'Germany:Stuttgart' }, - { name: 'Shakhtar', country: 'Ukraine', id: 'Ukraine:Shakhtar' }, - ], - [ - { name: 'Feyenoord', country: 'Netherlands', id: 'Netherlands:Feyenoord' }, - { name: 'Crvena zvezda', country: 'Serbia', id: 'Serbia:Crvena zvezda' }, - ], - [ - { name: 'Paris', country: 'France', id: 'France:Paris' }, - { name: 'Club Brugge', country: 'Belgium', id: 'Belgium:Club Brugge' }, - ], - [ - { name: 'Atlético', country: 'Spain', id: 'Spain:Atlético' }, - { name: 'Liverpool', country: 'England', id: 'England:Liverpool' }, - ], - [ - { - name: 'Slavia Praha', - country: 'Czech Republic', - id: 'Czech Republic:Slavia Praha', - }, - { name: 'Internazionale', country: 'Italy', id: 'Italy:Internazionale' }, - ], - [ - { name: 'PAOK', country: 'Greece', id: 'Greece:PAOK' }, - { name: 'Feyenoord', country: 'Netherlands', id: 'Netherlands:Feyenoord' }, - ], - [ - { name: 'Aston Villa', country: 'England', id: 'England:Aston Villa' }, - { name: 'Dortmund', country: 'Germany', id: 'Germany:Dortmund' }, - ], - [ - { name: 'PAOK', country: 'Greece', id: 'Greece:PAOK' }, - { name: 'Ferencváros', country: 'Hungary', id: 'Hungary:Ferencváros' }, - ], - [ - { name: 'Arsenal', country: 'England', id: 'England:Arsenal' }, - { name: 'Barcelona', country: 'Spain', id: 'Spain:Barcelona' }, - ], - [ - { name: 'Leverkusen', country: 'Germany', id: 'Germany:Leverkusen' }, - { name: 'Bologna', country: 'Italy', id: 'Italy:Bologna' }, - ], - [ - { name: 'Monaco', country: 'France', id: 'France:Monaco' }, - { name: 'Bologna', country: 'Italy', id: 'Italy:Bologna' }, - ], - [ - { name: 'Milan', country: 'Italy', id: 'Italy:Milan' }, - { name: 'Club Brugge', country: 'Belgium', id: 'Belgium:Club Brugge' }, - ], - [ - { name: 'Atlético', country: 'Spain', id: 'Spain:Atlético' }, - { name: 'Arsenal', country: 'England', id: 'England:Arsenal' }, - ], - [ - { name: 'Milan', country: 'Italy', id: 'Italy:Milan' }, - { name: 'Real Madrid', country: 'Spain', id: 'Spain:Real Madrid' }, - ], - [ - { name: 'Barcelona', country: 'Spain', id: 'Spain:Barcelona' }, - { name: 'Monaco', country: 'France', id: 'France:Monaco' }, - ], -]; diff --git a/src/pages/league/index.tsx b/src/pages/league/index.tsx index 81440fea..4bc0996c 100644 --- a/src/pages/league/index.tsx +++ b/src/pages/league/index.tsx @@ -1,13 +1,14 @@ -import { memo, useCallback, useEffect, useMemo, useState } from 'react'; -import styled, { css, keyframes } from 'styled-components'; +import { memo, useEffect, useMemo, useState } from 'react'; +import styled from 'styled-components'; import usePopup from '#store/usePopup'; import rawPots from '#experiments/pots'; import generatePairings from '#experiments/generatePairings'; import generateSchedule from '#experiments/generateSchedule'; -import getCountryFlagUrl from '#utils/getCountryFlagUrl'; +import Button from '#ui/Button'; -import games from './games'; +import Schedule from './Schedule'; +import MatchesTable from './MatchesTable'; const pots = rawPots.map(pot => pot.map(team => ({ @@ -20,265 +21,127 @@ type Team = (typeof pots)[number][number]; const Root = styled.div` display: flex; - gap: 16px; + flex-direction: column; margin: 10px; + font-size: 14px; `; -const Table = styled.table` - border-collapse: collapse; - border: 1px double rgb(128 128 128); - font-size: 10px; -`; - -const HeaderCell = styled.th<{ - hovered?: boolean; -}>` - vertical-align: bottom; - border: 1px solid rgb(192 192 192); - border-bottom-color: rgb(128 128 128); - padding: 3px 1px; - - &:nth-child(9n + 2) { - border-left: 1px double rgb(128 128 128); - } - - ${props => - props.hovered && - css` - background-color: rgba(0 0 0 / 0.1); - `} +const Interface = styled.div` + margin-bottom: 16px; `; -const HeaderCellDiv = styled.div` +const MatrixWrapper = styled.div` display: flex; - gap: 4px; - writing-mode: vertical-lr; - text-orientation: mixed; - font-weight: normal; - transform: rotate(180deg); - - > img { - width: 12px; - transform: rotate(90deg); - user-select: none; - pointer-events: none; - } -`; - -const BodyRow = styled.tr` - border: 1px solid rgb(192 192 192); - - &:hover { - background-color: rgba(0 0 0 / 0.1); - } - - &:nth-child(9n + 1) { - > td { - border-top: 1px double rgb(128 128 128); - } - } -`; - -const TeamCell = styled.td` - padding: 1px 3px; - border: 1px solid rgb(192 192 192); - - & + & { - text-align: center; - } - - &:nth-child(9n + 1) { - border-right: 1px double rgb(128 128 128); - } -`; - -const AppearLight = keyframes` - from { - background-color: rgb(255 255 0 / 0.5); - } -`; - -const TableCell = styled.td<{ - isMatch?: boolean; - hovered?: boolean; -}>` - border: 1px solid rgb(192 192 192); - - text-align: center; - - &:nth-child(9n + 1) { - border-right: 1px double rgb(128 128 128); - } - - ${props => - props.isMatch && - css` - animation: ${AppearLight} 3s ease-out normal forwards; - - &::before { - content: '✕'; - } - `} - - ${props => - props.hovered && - css` - background-color: rgba(0 0 0 / 0.1); - `} -`; - -const TeamDiv = styled.div<{ - country: string; -}>` - padding-left: 14.5px; - background-position-y: 1.5px; - background-size: 12px; - background-repeat: no-repeat; - - ${props => - props.country && - css` - background-image: url('${getCountryFlagUrl(props.country)}'); - `} + gap: 16px; `; function LeagueStage() { - const [, setPopup] = usePopup(); + const numMatchdays = 8; - const [hoverColumn, setHoverColumn] = useState(undefined); - - // @ts-expect-error Foo - const [pairings, setPairings] = useState<(readonly [Team, Team])[]>(games); - const [schedule, setSchedule] = useState<(readonly [Team, Team])[]>([]); - const [isFixturesDone, setIsFixturesDone] = useState(true); + const [, setPopup] = usePopup(); - console.log('pairings', JSON.stringify(pairings)); + const [isMatchdayMode, setIsMatchdayMode] = useState(false); + + const [pairings, setPairings] = useState<(readonly [Team, Team])[]>([]); + const [schedule, setSchedule] = useState< + readonly (readonly (readonly [Team, Team])[])[] + >( + Array.from( + { + length: numMatchdays, + }, + () => [], + ), + ); + const [isFixturesDone, setIsFixturesDone] = useState(false); const allTeams = useMemo(() => pots.flat(), []); + const matchdaySize = allTeams.length / 2; + useEffect(() => { setPopup({ waiting: false, }); }, []); - // useEffect(() => { - // const foo = async () => { - // const generator = generatePairings({ - // pots, - // numMatchdays: 8, - // isMatchPossible: (a, b) => a.country !== b.country, - // }); - // for await (const pickedMatch of generator) { - // setPairings(prev => [...prev, pickedMatch]); - // } - // console.log('pairings', JSON.stringify(pairings)); - // setIsFixturesDone(true); - // }; + useEffect(() => { + const formPairings = async () => { + const generator = generatePairings({ + pots, + numMatchdays: 8, + isMatchPossible: (a, b) => a.country !== b.country, + }); + for await (const pickedMatch of generator) { + setPairings(prev => [...prev, pickedMatch]); + } + console.log('pairings', JSON.stringify(pairings)); + setIsFixturesDone(true); + }; - // foo(); - // }, []); + formPairings(); + }, []); useEffect(() => { if (isFixturesDone) { - const foo = async () => { + const formSchedule = async () => { + // setIsMatchdayMode(true); + // setSchedule(chunk(pairings, 18)); const generator = generateSchedule({ - matchdaySize: allTeams.length / 2, - matches: pairings, + matchdaySize, + allGames: pairings, + currentSchedule: schedule, }); - for await (const pickedMatch of generator) { - setSchedule(prev => [...prev, pickedMatch]); + const iterator = await generator.next(); + if (iterator.done) { + throw new Error('Cannot be fully done'); } + const it = iterator.value; + setSchedule(it.solutionSchedule); + setIsMatchdayMode(true); }; - setTimeout(() => { - // foo(); - }, 2000); + formSchedule(); } }, [isFixturesDone]); - const pairingsMap = useMemo(() => { - const o: Record<`${string}:${string}`, boolean> = {}; - for (const pairing of pairings) { - o[`${pairing[0].name}:${pairing[1].name}`] = true; - } - return o; - }, [pairings]); - - const handleTableMouseOver = useCallback( - (e: React.MouseEvent) => { - const opponentId = - (e.target as HTMLTableCellElement).dataset.opponent || - ( - e.nativeEvent - .composedPath() - .find(el => (el as HTMLElement).dataset?.opponent) as - | HTMLElement - | undefined - )?.dataset?.opponent; - setHoverColumn(opponentId); - }, - [], - ); - - const handleTableMouseOut = useCallback( - (e: React.MouseEvent) => { - const opponentId = (e.target as HTMLTableCellElement).dataset.opponent; - if (opponentId) { - setHoverColumn(undefined); - } - }, - [], + const isScheduleDone = useMemo( + () => schedule.some(md => md.length > 0), + [schedule], ); return ( - - - - - {allTeams.map(opponent => ( - - - {`[${opponent.country}]`} - {opponent.name} - - - ))} - - - - {allTeams.map(team => ( - - - {team.name} - - {allTeams.map(opponent => { - const isMatch = pairingsMap[`${team.name}:${opponent.name}`]; - return ( - - ); - })} - - ))} - -
-
Drawn matches: {pairings.length}/144
+ + + + {isMatchdayMode ? ( + + ) : ( + + +
+
Drawn matches: {pairings.length}/144
+ {isFixturesDone && !isScheduleDone && ( +
+ The schedule is being generated. This will take a while. Do not + close the page +
+ )} +
+
+ )}
); } From ad9fd9dbfa1a5b703d014c92a8828f4084aaab9d Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Sun, 30 Jun 2024 21:55:23 +0100 Subject: [PATCH 15/41] upd --- src/experiments/{ => generatePairings}/generateFull.ts | 0 .../{ => generatePairings}/getFirstSuitableMatch.ts | 2 +- .../{ => generatePairings}/getFirstSuitableMatch.worker.ts | 0 .../{ => generatePairings}/getFirstSuitableMatch.wrapper.ts | 3 ++- .../{generatePairings.ts => generatePairings/index.ts} | 0 .../{ => generateSchedule}/getFirstSuitableMatchday.ts | 0 .../getFirstSuitableMatchday.worker.ts | 0 .../getFirstSuitableMatchday.wrapper.ts | 5 +++-- .../{generateSchedule.ts => generateSchedule/index.ts} | 0 src/{experiments => utils}/raceWorkers.ts | 0 10 files changed, 6 insertions(+), 4 deletions(-) rename src/experiments/{ => generatePairings}/generateFull.ts (100%) rename src/experiments/{ => generatePairings}/getFirstSuitableMatch.ts (99%) rename src/experiments/{ => generatePairings}/getFirstSuitableMatch.worker.ts (100%) rename src/experiments/{ => generatePairings}/getFirstSuitableMatch.wrapper.ts (95%) rename src/experiments/{generatePairings.ts => generatePairings/index.ts} (100%) rename src/experiments/{ => generateSchedule}/getFirstSuitableMatchday.ts (100%) rename src/experiments/{ => generateSchedule}/getFirstSuitableMatchday.worker.ts (100%) rename src/experiments/{ => generateSchedule}/getFirstSuitableMatchday.wrapper.ts (95%) rename src/experiments/{generateSchedule.ts => generateSchedule/index.ts} (100%) rename src/{experiments => utils}/raceWorkers.ts (100%) diff --git a/src/experiments/generateFull.ts b/src/experiments/generatePairings/generateFull.ts similarity index 100% rename from src/experiments/generateFull.ts rename to src/experiments/generatePairings/generateFull.ts diff --git a/src/experiments/getFirstSuitableMatch.ts b/src/experiments/generatePairings/getFirstSuitableMatch.ts similarity index 99% rename from src/experiments/getFirstSuitableMatch.ts rename to src/experiments/generatePairings/getFirstSuitableMatch.ts index 75624228..78648047 100644 --- a/src/experiments/getFirstSuitableMatch.ts +++ b/src/experiments/generatePairings/getFirstSuitableMatch.ts @@ -1,6 +1,6 @@ import { orderBy, shuffle } from 'lodash'; -import { findFirstSolution } from '../utils/backtrack'; +import { findFirstSolution } from '#utils/backtrack'; export default ({ numPots, diff --git a/src/experiments/getFirstSuitableMatch.worker.ts b/src/experiments/generatePairings/getFirstSuitableMatch.worker.ts similarity index 100% rename from src/experiments/getFirstSuitableMatch.worker.ts rename to src/experiments/generatePairings/getFirstSuitableMatch.worker.ts diff --git a/src/experiments/getFirstSuitableMatch.wrapper.ts b/src/experiments/generatePairings/getFirstSuitableMatch.wrapper.ts similarity index 95% rename from src/experiments/getFirstSuitableMatch.wrapper.ts rename to src/experiments/generatePairings/getFirstSuitableMatch.wrapper.ts index 1ec02734..a73a05eb 100644 --- a/src/experiments/getFirstSuitableMatch.wrapper.ts +++ b/src/experiments/generatePairings/getFirstSuitableMatch.wrapper.ts @@ -1,5 +1,6 @@ +import raceWorkers from '#utils/raceWorkers'; + import { type Func } from './getFirstSuitableMatch.worker'; -import raceWorkers from './raceWorkers'; const NUM_WORKERS = navigator.hardwareConcurrency - 1; diff --git a/src/experiments/generatePairings.ts b/src/experiments/generatePairings/index.ts similarity index 100% rename from src/experiments/generatePairings.ts rename to src/experiments/generatePairings/index.ts diff --git a/src/experiments/getFirstSuitableMatchday.ts b/src/experiments/generateSchedule/getFirstSuitableMatchday.ts similarity index 100% rename from src/experiments/getFirstSuitableMatchday.ts rename to src/experiments/generateSchedule/getFirstSuitableMatchday.ts diff --git a/src/experiments/getFirstSuitableMatchday.worker.ts b/src/experiments/generateSchedule/getFirstSuitableMatchday.worker.ts similarity index 100% rename from src/experiments/getFirstSuitableMatchday.worker.ts rename to src/experiments/generateSchedule/getFirstSuitableMatchday.worker.ts diff --git a/src/experiments/getFirstSuitableMatchday.wrapper.ts b/src/experiments/generateSchedule/getFirstSuitableMatchday.wrapper.ts similarity index 95% rename from src/experiments/getFirstSuitableMatchday.wrapper.ts rename to src/experiments/generateSchedule/getFirstSuitableMatchday.wrapper.ts index e243bf28..fb2f71b5 100644 --- a/src/experiments/getFirstSuitableMatchday.wrapper.ts +++ b/src/experiments/generateSchedule/getFirstSuitableMatchday.wrapper.ts @@ -1,6 +1,7 @@ import { remove, sample, shuffle, uniq } from 'lodash'; -import raceWorkers from './raceWorkers'; +import raceWorkers from '#utils/raceWorkers'; + import { type Func } from './getFirstSuitableMatchday.worker'; const NUM_WORKERS = navigator.hardwareConcurrency - 1; @@ -68,7 +69,7 @@ export default ({ }; }, getTimeout: (workerIndex, attempt) => { - const factor = 7 / (workerIndex + 1) + const factor = 7 / (workerIndex + 1); return factor * Math.min(30000, 5000 * Math.exp(attempt / 10)); }, }); diff --git a/src/experiments/generateSchedule.ts b/src/experiments/generateSchedule/index.ts similarity index 100% rename from src/experiments/generateSchedule.ts rename to src/experiments/generateSchedule/index.ts diff --git a/src/experiments/raceWorkers.ts b/src/utils/raceWorkers.ts similarity index 100% rename from src/experiments/raceWorkers.ts rename to src/utils/raceWorkers.ts From 73623bd8767069dc8a1bd1f4da6cfc2d64f644ab Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Sun, 30 Jun 2024 21:55:48 +0100 Subject: [PATCH 16/41] upd --- src/experiments/generatePairings/getFirstSuitableMatch.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/experiments/generatePairings/getFirstSuitableMatch.ts b/src/experiments/generatePairings/getFirstSuitableMatch.ts index 78648047..23f6b9c0 100644 --- a/src/experiments/generatePairings/getFirstSuitableMatch.ts +++ b/src/experiments/generatePairings/getFirstSuitableMatch.ts @@ -227,10 +227,9 @@ export default ({ }, }, ); - if (!solution) { - console.log('sol', solution); - } - // console.log('sol', solution); + // if (!solution) { + // console.log('sol', solution); + // } return solution !== undefined; })!; }; From dde796a4f875ec3c4b8824532b75d4f10ade7b2d Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Sun, 30 Jun 2024 21:56:58 +0100 Subject: [PATCH 17/41] upd --- src/pages/league/MatchesTable.tsx | 3 +-- src/pages/league/Schedule.tsx | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/league/MatchesTable.tsx b/src/pages/league/MatchesTable.tsx index e9249cd0..8aaf0193 100644 --- a/src/pages/league/MatchesTable.tsx +++ b/src/pages/league/MatchesTable.tsx @@ -14,9 +14,9 @@ const HeaderCell = styled.th<{ hovered?: boolean; }>` vertical-align: bottom; + padding: 3px 1px; border: 1px solid rgb(192 192 192); border-bottom-color: rgb(128 128 128); - padding: 3px 1px; &:nth-child(9n + 2) { border-left: 1px double rgb(128 128 128); @@ -83,7 +83,6 @@ const TableCell = styled.td<{ hovered?: boolean; }>` border: 1px solid rgb(192 192 192); - text-align: center; &:nth-child(9n + 1) { diff --git a/src/pages/league/Schedule.tsx b/src/pages/league/Schedule.tsx index 79228852..0855095e 100644 --- a/src/pages/league/Schedule.tsx +++ b/src/pages/league/Schedule.tsx @@ -39,8 +39,8 @@ const MatchdayHeader = styled.div` const MatchPair = styled.div` display: flex; align-items: center; - border-top: 1px solid rgb(192 192 192); height: 20px; + border-top: 1px solid rgb(192 192 192); > :first-child { align-items: end; From 2d53e4c03e787b10243c05eac405b21632478526 Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Sun, 30 Jun 2024 23:13:27 +0100 Subject: [PATCH 18/41] <=2 --- .../generatePairings/getFirstSuitableMatch.ts | 41 +++++++++++++++++++ src/experiments/generatePairings/index.ts | 2 + 2 files changed, 43 insertions(+) diff --git a/src/experiments/generatePairings/getFirstSuitableMatch.ts b/src/experiments/generatePairings/getFirstSuitableMatch.ts index 23f6b9c0..ff5e24be 100644 --- a/src/experiments/generatePairings/getFirstSuitableMatch.ts +++ b/src/experiments/generatePairings/getFirstSuitableMatch.ts @@ -1,8 +1,14 @@ import { orderBy, shuffle } from 'lodash'; import { findFirstSolution } from '#utils/backtrack'; +import { type Country } from '#model/types'; + +interface Team { + country: Country; +} export default ({ + teams, numPots, numTeamsPerPot, numMatchdays, @@ -12,6 +18,7 @@ export default ({ randomArray, shouldShuffle, }: { + teams: readonly Team[]; numPots: number; numTeamsPerPot: number; numMatchdays: number; @@ -25,6 +32,7 @@ export default ({ const numHomeGamesByTeam: Record = {}; const numAwayGamesByTeam: Record = {}; + const numOpponentCountriesByTeam: Record<`${number}:${Country}`, number> = {}; /** * team:pot:home? @@ -35,10 +43,17 @@ export default ({ > = {}; for (const m of pickedMatches) { + const homeTeam = teams[m[0]]; + const awayTeam = teams[m[1]]; + const homePot = Math.floor(m[0] / numTeamsPerPot); const awayPot = Math.floor(m[1] / numTeamsPerPot); numHomeGamesByTeam[m[0]] = (numHomeGamesByTeam[m[0]] ?? 0) + 1; numAwayGamesByTeam[m[1]] = (numAwayGamesByTeam[m[1]] ?? 0) + 1; + numOpponentCountriesByTeam[`${m[0]}:${awayTeam.country}`] = + (numOpponentCountriesByTeam[`${m[0]}:${awayTeam.country}`] ?? 0) + 1; + numOpponentCountriesByTeam[`${m[1]}:${homeTeam.country}`] = + (numOpponentCountriesByTeam[`${m[1]}:${homeTeam.country}`] ?? 0) + 1; hasPlayedWithPotMap[`${m[0]}:${awayPot}:h`] = true; hasPlayedWithPotMap[`${m[1]}:${homePot}:a`] = true; } @@ -93,6 +108,7 @@ export default ({ target: pickedMatches, numHomeGamesByTeam, numAwayGamesByTeam, + numOpponentCountriesByTeam, hasPlayedWithPotMap, picked: m, }, @@ -119,6 +135,18 @@ export default ({ return true; } + if ( + c.numOpponentCountriesByTeam[`${m1}:${teams[m2].country}`] === 2 + ) { + return true; + } + + if ( + c.numOpponentCountriesByTeam[`${m2}:${teams[m1].country}`] === 2 + ) { + return true; + } + return false; }, @@ -135,6 +163,18 @@ export default ({ [c.picked[1]]: (c.numAwayGamesByTeam[c.picked[1]] ?? 0) + 1, } as typeof c.numAwayGamesByTeam; + const newNumOpponentCountriesByTeam = { + ...c.numOpponentCountriesByTeam, + [`${c.picked[0]}:${teams[c.picked[1]].country}`]: + (c.numOpponentCountriesByTeam[ + `${c.picked[0]}:${teams[c.picked[1]].country}` + ] ?? 0) + 1, + [`${c.picked[1]}:${teams[c.picked[0]].country}`]: + (c.numOpponentCountriesByTeam[ + `${c.picked[1]}:${teams[c.picked[0]].country}` + ] ?? 0) + 1, + }; + const pickedHomePotIndex = Math.floor(c.picked[0] / numTeamsPerPot); const pickedAwayPotIndex = Math.floor(c.picked[1] / numTeamsPerPot); @@ -217,6 +257,7 @@ export default ({ picked: newPicked, numHomeGamesByTeam: newNumHomeGamesByTeam, numAwayGamesByTeam: newNumAwayGamesByTeam, + numOpponentCountriesByTeam: newNumOpponentCountriesByTeam, hasPlayedWithPotMap: newHasPlayedWithPotMap, }); } diff --git a/src/experiments/generatePairings/index.ts b/src/experiments/generatePairings/index.ts index 9ebe0fc1..e55bb43c 100644 --- a/src/experiments/generatePairings/index.ts +++ b/src/experiments/generatePairings/index.ts @@ -49,6 +49,8 @@ export default async function* generatePairings({ while (matches.length < numMatchdays * numGamesPerMatchday) { // eslint-disable-next-line no-await-in-loop const pickedMatch = await getFirstSuitableMatch({ + // @ts-expect-error Fix this later + teams, numPots: pots.length, numTeamsPerPot, numMatchdays, From 62d1eccb13c88e273809b1f51ccf34c23a92d218 Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Mon, 1 Jul 2024 12:51:39 +0100 Subject: [PATCH 19/41] shuffle --- src/pages/league/index.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pages/league/index.tsx b/src/pages/league/index.tsx index 4bc0996c..7b1abeb3 100644 --- a/src/pages/league/index.tsx +++ b/src/pages/league/index.tsx @@ -1,5 +1,6 @@ import { memo, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; +import { shuffle } from 'lodash'; import usePopup from '#store/usePopup'; import rawPots from '#experiments/pots'; @@ -43,9 +44,7 @@ function LeagueStage() { const [isMatchdayMode, setIsMatchdayMode] = useState(false); const [pairings, setPairings] = useState<(readonly [Team, Team])[]>([]); - const [schedule, setSchedule] = useState< - readonly (readonly (readonly [Team, Team])[])[] - >( + const [schedule, setSchedule] = useState<(readonly [Team, Team])[][]>( Array.from( { length: numMatchdays, @@ -97,7 +96,7 @@ function LeagueStage() { throw new Error('Cannot be fully done'); } const it = iterator.value; - setSchedule(it.solutionSchedule); + setSchedule(it.solutionSchedule.map(md => shuffle(md))); setIsMatchdayMode(true); }; From d455b19a0a5e92c95e3a030521a3225df1be1768 Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Mon, 1 Jul 2024 12:54:06 +0100 Subject: [PATCH 20/41] upd --- .../generatePairings/getFirstSuitableMatch.wrapper.ts | 2 +- .../generateSchedule/getFirstSuitableMatchday.wrapper.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/experiments/generatePairings/getFirstSuitableMatch.wrapper.ts b/src/experiments/generatePairings/getFirstSuitableMatch.wrapper.ts index a73a05eb..1db521fd 100644 --- a/src/experiments/generatePairings/getFirstSuitableMatch.wrapper.ts +++ b/src/experiments/generatePairings/getFirstSuitableMatch.wrapper.ts @@ -27,6 +27,6 @@ export default async ( }, getTimeout: (workerIndex, iteration) => { const factor = 7 / (workerIndex + 1); - return factor * Math.min(10000, 1000 * Math.exp(iteration / 10)); + return factor * Math.min(5000, 1000 * Math.exp(iteration / 10)); }, }); diff --git a/src/experiments/generateSchedule/getFirstSuitableMatchday.wrapper.ts b/src/experiments/generateSchedule/getFirstSuitableMatchday.wrapper.ts index fb2f71b5..39b95a88 100644 --- a/src/experiments/generateSchedule/getFirstSuitableMatchday.wrapper.ts +++ b/src/experiments/generateSchedule/getFirstSuitableMatchday.wrapper.ts @@ -70,6 +70,6 @@ export default ({ }, getTimeout: (workerIndex, attempt) => { const factor = 7 / (workerIndex + 1); - return factor * Math.min(30000, 5000 * Math.exp(attempt / 10)); + return factor * Math.min(10000, 5000 * Math.exp(attempt / 10)); }, }); From 364a4f517e560ac9f6b6061335eb10a966769cc0 Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Mon, 1 Jul 2024 12:56:02 +0100 Subject: [PATCH 21/41] num workers --- .../generatePairings/getFirstSuitableMatch.wrapper.ts | 2 +- .../generateSchedule/getFirstSuitableMatchday.wrapper.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/experiments/generatePairings/getFirstSuitableMatch.wrapper.ts b/src/experiments/generatePairings/getFirstSuitableMatch.wrapper.ts index 1db521fd..fe650ddc 100644 --- a/src/experiments/generatePairings/getFirstSuitableMatch.wrapper.ts +++ b/src/experiments/generatePairings/getFirstSuitableMatch.wrapper.ts @@ -2,7 +2,7 @@ import raceWorkers from '#utils/raceWorkers'; import { type Func } from './getFirstSuitableMatch.worker'; -const NUM_WORKERS = navigator.hardwareConcurrency - 1; +const NUM_WORKERS = Math.max(1, navigator.hardwareConcurrency - 1); export default async ( options: Omit[0], 'randomArray' | 'shouldShuffle'>, diff --git a/src/experiments/generateSchedule/getFirstSuitableMatchday.wrapper.ts b/src/experiments/generateSchedule/getFirstSuitableMatchday.wrapper.ts index 39b95a88..d9fc1d6c 100644 --- a/src/experiments/generateSchedule/getFirstSuitableMatchday.wrapper.ts +++ b/src/experiments/generateSchedule/getFirstSuitableMatchday.wrapper.ts @@ -4,7 +4,7 @@ import raceWorkers from '#utils/raceWorkers'; import { type Func } from './getFirstSuitableMatchday.worker'; -const NUM_WORKERS = navigator.hardwareConcurrency - 1; +const NUM_WORKERS = Math.max(1, navigator.hardwareConcurrency - 1); export default ({ matchdaySize, From d4bff51e14d2c5e12b661c0c0a03627b7e2b9213 Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Mon, 1 Jul 2024 12:58:33 +0100 Subject: [PATCH 22/41] upd --- .../getFirstSuitableMatchday.wrapper.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/experiments/generateSchedule/getFirstSuitableMatchday.wrapper.ts b/src/experiments/generateSchedule/getFirstSuitableMatchday.wrapper.ts index d9fc1d6c..8db8d91a 100644 --- a/src/experiments/generateSchedule/getFirstSuitableMatchday.wrapper.ts +++ b/src/experiments/generateSchedule/getFirstSuitableMatchday.wrapper.ts @@ -36,7 +36,7 @@ export default ({ ++matchesByTeam[a]; } - const res: typeof allGamesShuffled = []; + const orderedGames: typeof allGamesShuffled = []; while (allGamesShuffled.length > 0) { const min = Math.min(...matchesByTeam.filter(item => item > 0)); const minIndices: number[] = []; @@ -54,16 +54,12 @@ export default ({ --matchesByTeam[m[0]]; --matchesByTeam[m[1]]; } - res.push(...minTeamMatches); + orderedGames.push(...minTeamMatches); } - // const indexByTeam = new Map( - // allTeamsShuffled.map((item, index) => [item, index]), - // ); - return { matchdaySize, - allGames: res, + allGames: orderedGames, currentSchedule, matchIndex, }; From a0ec9bea43ba68371b7489f05d7519ff2459e38f Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Mon, 1 Jul 2024 18:16:40 +0100 Subject: [PATCH 23/41] integrate --- src/config.ts | 2 +- src/data/cl/ls/2024/pots.json | 154 ++++++++++++++++++ src/experiments/pots.ts | 157 ------------------- src/model/Stage.ts | 2 +- src/pages/{league => cl/ls}/MatchesTable.tsx | 0 src/pages/{league => cl/ls}/Schedule.tsx | 0 src/pages/{league => cl/ls}/index.tsx | 30 ++-- src/routes/currentSeasonByTournament.ts | 10 +- src/routes/index.tsx | 7 +- 9 files changed, 183 insertions(+), 179 deletions(-) create mode 100644 src/data/cl/ls/2024/pots.json delete mode 100644 src/experiments/pots.ts rename src/pages/{league => cl/ls}/MatchesTable.tsx (100%) rename src/pages/{league => cl/ls}/Schedule.tsx (100%) rename src/pages/{league => cl/ls}/index.tsx (88%) diff --git a/src/config.ts b/src/config.ts index ef1dc59b..615a8a90 100644 --- a/src/config.ts +++ b/src/config.ts @@ -6,7 +6,7 @@ export default { wc: 2022, uefa: { cl: { - gs: 2023, + ls: 2024, ko: 2023, }, el: { diff --git a/src/data/cl/ls/2024/pots.json b/src/data/cl/ls/2024/pots.json new file mode 100644 index 00000000..0e9edef9 --- /dev/null +++ b/src/data/cl/ls/2024/pots.json @@ -0,0 +1,154 @@ +[ + [ + { + "name": "Real Madrid", + "country": "Spain" + }, + { + "name": "Man City", + "country": "England" + }, + { + "name": "Bayern", + "country": "Germany" + }, + { + "name": "Paris", + "country": "France" + }, + { + "name": "Liverpool", + "country": "England" + }, + { + "name": "Internazionale", + "country": "Italy" + }, + { + "name": "Dortmund", + "country": "Germany" + }, + { + "name": "Leipzig", + "country": "Germany" + }, + { + "name": "Barcelona", + "country": "Spain" + } + ], + [ + { + "name": "Leverkusen", + "country": "Germany" + }, + { + "name": "Atlético", + "country": "Spain" + }, + { + "name": "Atalanta", + "country": "Italy" + }, + { + "name": "Juventus", + "country": "Italy" + }, + { + "name": "Benfica", + "country": "Portugal" + }, + { + "name": "Arsenal", + "country": "England" + }, + { + "name": "Club Brugge", + "country": "Belgium" + }, + { + "name": "Rangers", + "country": "Scotland" + }, + { + "name": "Shakhtar", + "country": "Ukraine" + } + ], + [ + { + "name": "Milan", + "country": "Italy" + }, + { + "name": "Feyenoord", + "country": "Netherlands" + }, + { + "name": "Sporting CP", + "country": "Portugal" + }, + { + "name": "PSV", + "country": "Netherlands" + }, + { + "name": "Slavia Praha", + "country": "Czechia" + }, + { + "name": "Dinamo Zagreb", + "country": "Croatia" + }, + { + "name": "Crvena zvezda", + "country": "Serbia" + }, + { + "name": "PAOK", + "country": "Greece" + }, + { + "name": "M Tel-Aviv", + "country": "Israel" + } + ], + [ + { + "name": "Ferencváros", + "country": "Hungary" + }, + { + "name": "Celtic", + "country": "Scotland" + }, + { + "name": "Monaco", + "country": "France" + }, + { + "name": "Aston Villa", + "country": "England" + }, + { + "name": "Bologna", + "country": "Italy" + }, + { + "name": "Girona", + "country": "Spain" + }, + { + "name": "Stuttgart", + "country": "Germany" + }, + { + "name": "Sturm", + "country": "Austria" + }, + { + "name": "Brest", + "country": "France" + } + ] +] diff --git a/src/experiments/pots.ts b/src/experiments/pots.ts deleted file mode 100644 index f59fd076..00000000 --- a/src/experiments/pots.ts +++ /dev/null @@ -1,157 +0,0 @@ -export default [ - [ - { - name: 'Real Madrid', - country: 'Spain', - }, - { - name: 'Man City', - country: 'England', - }, - { - name: 'Bayern', - country: 'Germany', - }, - { - name: 'Paris', - country: 'France', - }, - { - name: 'Liverpool', - country: 'England', - }, - { - name: 'Internazionale', - country: 'Italy', - }, - { - name: 'Dortmund', - country: 'Germany', - }, - { - name: 'Leipzig', - country: 'Germany', - }, - { - name: 'Barcelona', - country: 'Spain', - }, - ], - [ - { - name: 'Leverkusen', - country: 'Germany', - }, - { - name: 'Atlético', - country: 'Spain', - }, - { - name: 'Atalanta', - country: 'Italy', - }, - { - name: 'Juventus', - country: 'Italy', - }, - { - name: 'Benfica', - country: 'Portugal', - }, - { - name: 'Arsenal', - country: 'England', - }, - { - name: 'Club Brugge', - country: 'Belgium', - }, - { - name: 'Rangers', - country: 'Scotland', - }, - { - name: 'Shakhtar', - country: 'Ukraine', - }, - ], - [ - { - name: 'Milan', - country: 'Italy', - }, - { - name: 'Feyenoord', - country: 'Netherlands', - }, - { - name: 'Sporting CP', - country: 'Portugal', - }, - { - name: 'PSV', - country: 'Netherlands', - }, - { - name: 'Slavia Praha', - country: 'Czechia', - }, - { - name: 'Dinamo Zagreb', - country: 'Croatia', - }, - { - name: 'Crvena zvezda', - country: 'Serbia', - }, - { - name: 'PAOK', - country: 'Greece', - }, - { - name: 'M Tel-Aviv', - country: 'Israel', - }, - ], - [ - { - name: 'Ferencváros', - country: 'Hungary', - }, - { - name: 'Celtic', - country: 'Scotland', - }, - { - name: 'Monaco', - country: 'France', - }, - { - name: 'Aston Villa', - country: 'England', - }, - { - name: 'Bologna', - country: 'Italy', - }, - { - name: 'Girona', - country: 'Spain', - }, - { - name: 'Stuttgart', - country: 'Germany', - }, - { - name: 'Sturm', - country: 'Austria', - }, - { - name: 'Brest', - country: 'France', - }, - ], -] as const satisfies readonly (readonly { - name: string; - country: string; -}[])[]; diff --git a/src/model/Stage.ts b/src/model/Stage.ts index be33fdf3..809ed16b 100644 --- a/src/model/Stage.ts +++ b/src/model/Stage.ts @@ -1,4 +1,4 @@ -const validStages = ['gs', 'ko'] as const; +const validStages = ['gs', 'ko', 'ls'] as const; type Stage = (typeof validStages)[number]; diff --git a/src/pages/league/MatchesTable.tsx b/src/pages/cl/ls/MatchesTable.tsx similarity index 100% rename from src/pages/league/MatchesTable.tsx rename to src/pages/cl/ls/MatchesTable.tsx diff --git a/src/pages/league/Schedule.tsx b/src/pages/cl/ls/Schedule.tsx similarity index 100% rename from src/pages/league/Schedule.tsx rename to src/pages/cl/ls/Schedule.tsx diff --git a/src/pages/league/index.tsx b/src/pages/cl/ls/index.tsx similarity index 88% rename from src/pages/league/index.tsx rename to src/pages/cl/ls/index.tsx index 7b1abeb3..403d9ad9 100644 --- a/src/pages/league/index.tsx +++ b/src/pages/cl/ls/index.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components'; import { shuffle } from 'lodash'; import usePopup from '#store/usePopup'; -import rawPots from '#experiments/pots'; +import type Team from '#model/team/GsTeam'; import generatePairings from '#experiments/generatePairings'; import generateSchedule from '#experiments/generateSchedule'; import Button from '#ui/Button'; @@ -11,15 +11,6 @@ import Button from '#ui/Button'; import Schedule from './Schedule'; import MatchesTable from './MatchesTable'; -const pots = rawPots.map(pot => - pot.map(team => ({ - ...team, - id: `${team.country}:${team.name}`, - })), -); - -type Team = (typeof pots)[number][number]; - const Root = styled.div` display: flex; flex-direction: column; @@ -36,7 +27,11 @@ const MatrixWrapper = styled.div` gap: 16px; `; -function LeagueStage() { +interface Props { + pots: readonly (readonly Team[])[]; +} + +function LeagueStage({ pots: initialPots }: Props) { const numMatchdays = 8; const [, setPopup] = usePopup(); @@ -54,7 +49,18 @@ function LeagueStage() { ); const [isFixturesDone, setIsFixturesDone] = useState(false); - const allTeams = useMemo(() => pots.flat(), []); + const pots = useMemo( + () => + initialPots.map(pot => + pot.map(team => ({ + ...team, + id: `${team.country}:${team.name}`, + })), + ), + [initialPots], + ); + + const allTeams = useMemo(() => pots.flat(), [pots]); const matchdaySize = allTeams.length / 2; diff --git a/src/routes/currentSeasonByTournament.ts b/src/routes/currentSeasonByTournament.ts index a4f56bb3..f2484e68 100644 --- a/src/routes/currentSeasonByTournament.ts +++ b/src/routes/currentSeasonByTournament.ts @@ -5,5 +5,11 @@ import config from '../config'; const { wc, uefa } = config.currentSeason; -export default (tournament: Tournament | null, stage: Stage | null): number => - tournament === 'wc' ? wc : uefa[tournament || 'cl'][stage || 'gs']; +export default (tournament: Tournament | null, stage: Stage | null): number => { + const resolvedTournament = tournament || 'cl'; + const resolvedState = stage || (resolvedTournament === 'cl' ? 'ls' : 'gs'); + return resolvedTournament === 'wc' + ? wc + : // @ts-expect-error Fix later + uefa[resolvedTournament][resolvedState]; +}; diff --git a/src/routes/index.tsx b/src/routes/index.tsx index e9c3168e..f1bac03c 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -15,7 +15,6 @@ import useDrawId from '#store/useDrawId'; import usePopup from '#store/usePopup'; import config from '../config'; -import League from '../pages/league'; import HeadMetadata from './HeadMetadata'; import Navbar from './Navbar'; @@ -96,10 +95,6 @@ function Routing() { ) : null} {/* TODO */} - } - /> } From 07f2e5450e06a290b956a9179cf129518aa99be4 Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Mon, 1 Jul 2024 18:28:08 +0100 Subject: [PATCH 24/41] integrate ui --- src/pages/cl/ls/index.tsx | 12 ++++++------ src/routes/Navbar/index.tsx | 19 +++++++++++++------ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/pages/cl/ls/index.tsx b/src/pages/cl/ls/index.tsx index 403d9ad9..88341a26 100644 --- a/src/pages/cl/ls/index.tsx +++ b/src/pages/cl/ls/index.tsx @@ -7,6 +7,7 @@ import type Team from '#model/team/GsTeam'; import generatePairings from '#experiments/generatePairings'; import generateSchedule from '#experiments/generateSchedule'; import Button from '#ui/Button'; +import Portal from '#ui/Portal'; import Schedule from './Schedule'; import MatchesTable from './MatchesTable'; @@ -18,10 +19,6 @@ const Root = styled.div` font-size: 14px; `; -const Interface = styled.div` - margin-bottom: 16px; -`; - const MatrixWrapper = styled.div` display: flex; gap: 16px; @@ -117,7 +114,10 @@ function LeagueStage({ pots: initialPots }: Props) { return ( - + - + {isMatchdayMode ? ( ) : ( diff --git a/src/routes/Navbar/index.tsx b/src/routes/Navbar/index.tsx index 34f81507..b55c8c70 100644 --- a/src/routes/Navbar/index.tsx +++ b/src/routes/Navbar/index.tsx @@ -52,6 +52,10 @@ const Root = styled.div` } `; +const NavBarLeftContainer = styled.div` + display: contents; +`; + interface Props { season: number; tournament: Tournament; @@ -83,14 +87,17 @@ function Navbar({ season, tournament, stage, onSeasonChange }: Props) { return ( +
- + {stage !== 'ls' && ( + + )} Date: Mon, 1 Jul 2024 18:42:05 +0100 Subject: [PATCH 25/41] restructure --- .../dfs/ls}/generatePairings/generateFull.ts | 0 .../dfs/ls}/generatePairings/getFirstSuitableMatch.ts | 0 .../dfs/ls}/generatePairings/getFirstSuitableMatch.worker.ts | 0 .../dfs/ls}/generatePairings/getFirstSuitableMatch.wrapper.ts | 0 src/{experiments => engine/dfs/ls}/generatePairings/index.ts | 0 .../dfs/ls}/generateSchedule/getFirstSuitableMatchday.ts | 0 .../ls}/generateSchedule/getFirstSuitableMatchday.worker.ts | 0 .../ls}/generateSchedule/getFirstSuitableMatchday.wrapper.ts | 0 src/{experiments => engine/dfs/ls}/generateSchedule/index.ts | 0 src/pages/cl/ls/index.tsx | 4 ++-- 10 files changed, 2 insertions(+), 2 deletions(-) rename src/{experiments => engine/dfs/ls}/generatePairings/generateFull.ts (100%) rename src/{experiments => engine/dfs/ls}/generatePairings/getFirstSuitableMatch.ts (100%) rename src/{experiments => engine/dfs/ls}/generatePairings/getFirstSuitableMatch.worker.ts (100%) rename src/{experiments => engine/dfs/ls}/generatePairings/getFirstSuitableMatch.wrapper.ts (100%) rename src/{experiments => engine/dfs/ls}/generatePairings/index.ts (100%) rename src/{experiments => engine/dfs/ls}/generateSchedule/getFirstSuitableMatchday.ts (100%) rename src/{experiments => engine/dfs/ls}/generateSchedule/getFirstSuitableMatchday.worker.ts (100%) rename src/{experiments => engine/dfs/ls}/generateSchedule/getFirstSuitableMatchday.wrapper.ts (100%) rename src/{experiments => engine/dfs/ls}/generateSchedule/index.ts (100%) diff --git a/src/experiments/generatePairings/generateFull.ts b/src/engine/dfs/ls/generatePairings/generateFull.ts similarity index 100% rename from src/experiments/generatePairings/generateFull.ts rename to src/engine/dfs/ls/generatePairings/generateFull.ts diff --git a/src/experiments/generatePairings/getFirstSuitableMatch.ts b/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.ts similarity index 100% rename from src/experiments/generatePairings/getFirstSuitableMatch.ts rename to src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.ts diff --git a/src/experiments/generatePairings/getFirstSuitableMatch.worker.ts b/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.worker.ts similarity index 100% rename from src/experiments/generatePairings/getFirstSuitableMatch.worker.ts rename to src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.worker.ts diff --git a/src/experiments/generatePairings/getFirstSuitableMatch.wrapper.ts b/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.wrapper.ts similarity index 100% rename from src/experiments/generatePairings/getFirstSuitableMatch.wrapper.ts rename to src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.wrapper.ts diff --git a/src/experiments/generatePairings/index.ts b/src/engine/dfs/ls/generatePairings/index.ts similarity index 100% rename from src/experiments/generatePairings/index.ts rename to src/engine/dfs/ls/generatePairings/index.ts diff --git a/src/experiments/generateSchedule/getFirstSuitableMatchday.ts b/src/engine/dfs/ls/generateSchedule/getFirstSuitableMatchday.ts similarity index 100% rename from src/experiments/generateSchedule/getFirstSuitableMatchday.ts rename to src/engine/dfs/ls/generateSchedule/getFirstSuitableMatchday.ts diff --git a/src/experiments/generateSchedule/getFirstSuitableMatchday.worker.ts b/src/engine/dfs/ls/generateSchedule/getFirstSuitableMatchday.worker.ts similarity index 100% rename from src/experiments/generateSchedule/getFirstSuitableMatchday.worker.ts rename to src/engine/dfs/ls/generateSchedule/getFirstSuitableMatchday.worker.ts diff --git a/src/experiments/generateSchedule/getFirstSuitableMatchday.wrapper.ts b/src/engine/dfs/ls/generateSchedule/getFirstSuitableMatchday.wrapper.ts similarity index 100% rename from src/experiments/generateSchedule/getFirstSuitableMatchday.wrapper.ts rename to src/engine/dfs/ls/generateSchedule/getFirstSuitableMatchday.wrapper.ts diff --git a/src/experiments/generateSchedule/index.ts b/src/engine/dfs/ls/generateSchedule/index.ts similarity index 100% rename from src/experiments/generateSchedule/index.ts rename to src/engine/dfs/ls/generateSchedule/index.ts diff --git a/src/pages/cl/ls/index.tsx b/src/pages/cl/ls/index.tsx index 88341a26..52b26289 100644 --- a/src/pages/cl/ls/index.tsx +++ b/src/pages/cl/ls/index.tsx @@ -4,8 +4,8 @@ import { shuffle } from 'lodash'; import usePopup from '#store/usePopup'; import type Team from '#model/team/GsTeam'; -import generatePairings from '#experiments/generatePairings'; -import generateSchedule from '#experiments/generateSchedule'; +import generatePairings from '#engine/dfs/ls/generatePairings/index.js'; +import generateSchedule from '#engine/dfs/ls/generateSchedule/index.js'; import Button from '#ui/Button'; import Portal from '#ui/Portal'; From a6d652b2eaab6396ba5d2f0f37f19dc9a471685f Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Mon, 1 Jul 2024 19:16:07 +0100 Subject: [PATCH 26/41] upd --- src/pages/cl/ls/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/cl/ls/index.tsx b/src/pages/cl/ls/index.tsx index 52b26289..146082f6 100644 --- a/src/pages/cl/ls/index.tsx +++ b/src/pages/cl/ls/index.tsx @@ -4,8 +4,8 @@ import { shuffle } from 'lodash'; import usePopup from '#store/usePopup'; import type Team from '#model/team/GsTeam'; -import generatePairings from '#engine/dfs/ls/generatePairings/index.js'; -import generateSchedule from '#engine/dfs/ls/generateSchedule/index.js'; +import generatePairings from '#engine/dfs/ls/generatePairings/index'; +import generateSchedule from '#engine/dfs/ls/generateSchedule/index'; import Button from '#ui/Button'; import Portal from '#ui/Portal'; From aa3e0ecd40ee74397138b9aaa3392f2e8f02528a Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Mon, 1 Jul 2024 21:59:06 +0100 Subject: [PATCH 27/41] milan & inter --- .../getFirstSuitableMatchday.ts | 40 ++++++++++++++++ .../getFirstSuitableMatchday.wrapper.ts | 46 ++++++++++++++----- src/engine/dfs/ls/generateSchedule/index.ts | 9 ++-- .../generateSchedule/teamsSharingStadium.ts | 1 + 4 files changed, 82 insertions(+), 14 deletions(-) create mode 100644 src/engine/dfs/ls/generateSchedule/teamsSharingStadium.ts diff --git a/src/engine/dfs/ls/generateSchedule/getFirstSuitableMatchday.ts b/src/engine/dfs/ls/generateSchedule/getFirstSuitableMatchday.ts index 4438fe9f..51e25a19 100644 --- a/src/engine/dfs/ls/generateSchedule/getFirstSuitableMatchday.ts +++ b/src/engine/dfs/ls/generateSchedule/getFirstSuitableMatchday.ts @@ -2,12 +2,20 @@ import { range, shuffle } from 'lodash'; import { findFirstSolution } from '#utils/backtrack'; +import teamsSharingStadium from './teamsSharingStadium'; + +interface Team { + name: string; +} + export default ({ + teams, matchdaySize, allGames, currentSchedule, matchIndex, }: { + teams: readonly Team[]; matchdaySize: number; allGames: readonly (readonly [number, number])[]; currentSchedule: Record<`${number}:${number}`, number>; @@ -16,6 +24,18 @@ export default ({ const numGames = allGames.length; const numMatchdays = numGames / matchdaySize; + const indexByTeamName = new Map(teams.map((team, i) => [team.name, i])); + + const sameStadiumTeamMap = new Map(); + for (const pair of teamsSharingStadium) { + const a = indexByTeamName.get(pair[0]); + const b = indexByTeamName.get(pair[1]); + if (a !== undefined && b !== undefined) { + sameStadiumTeamMap.set(a, b); + sameStadiumTeamMap.set(b, a); + } + } + let record = 0; const numMatchesPerMatchday = Array.from( @@ -71,6 +91,26 @@ export default ({ return true; } + const homeSameStadiumTeam = sameStadiumTeamMap.get(h); + if ( + homeSameStadiumTeam !== undefined && + c.locationByMatchday[ + `${homeSameStadiumTeam}:${c.pickedMatchday}` + ] === 'h' + ) { + return true; + } + + const awaySameStadiumTeam = sameStadiumTeamMap.get(a); + if ( + awaySameStadiumTeam !== undefined && + c.locationByMatchday[ + `${awaySameStadiumTeam}:${c.pickedMatchday}` + ] === 'a' + ) { + return true; + } + for (let b = 0; b < 2; ++b) { const loc = b === 0 ? 'h' : 'a'; const t = b === 0 ? h : a; diff --git a/src/engine/dfs/ls/generateSchedule/getFirstSuitableMatchday.wrapper.ts b/src/engine/dfs/ls/generateSchedule/getFirstSuitableMatchday.wrapper.ts index 8db8d91a..4d8b248c 100644 --- a/src/engine/dfs/ls/generateSchedule/getFirstSuitableMatchday.wrapper.ts +++ b/src/engine/dfs/ls/generateSchedule/getFirstSuitableMatchday.wrapper.ts @@ -1,17 +1,24 @@ -import { remove, sample, shuffle, uniq } from 'lodash'; +import { remove, sample, shuffle } from 'lodash'; import raceWorkers from '#utils/raceWorkers'; import { type Func } from './getFirstSuitableMatchday.worker'; +import teamsSharingStadium from './teamsSharingStadium'; const NUM_WORKERS = Math.max(1, navigator.hardwareConcurrency - 1); +interface Team { + name: string; +} + export default ({ + teams, matchdaySize, allGames, currentSchedule, matchIndex, }: { + teams: readonly Team[]; matchdaySize: number; allGames: readonly (readonly [number, number])[]; currentSchedule: Record<`${number}:${number}`, number>; @@ -23,24 +30,40 @@ export default ({ new Worker(new URL('./getFirstSuitableMatchday.worker', import.meta.url)), getPayload: () => { const allGamesShuffled = shuffle(allGames); - const allTeamsShuffled = uniq(allGames.flat()); - const matchesByTeam = Array.from( + + const prioritizedTeams = teamsSharingStadium.flatMap(namePair => { + const [a, b] = namePair; + return [teams.find(t => t.name === a)!, teams.find(t => t.name === b)!]; + }); + + const orderedGames: typeof allGamesShuffled = []; + for (const team of prioritizedTeams) { + const hah = remove(allGamesShuffled, m => { + const h = teams[m[0]]; + const a = teams[m[1]]; + return team === h || team === a; + }); + orderedGames.push(...hah); + } + + console.log('remaining after priority', allGamesShuffled); + + const numMatchesByTeam = Array.from( { - length: allTeamsShuffled.length, + length: teams.length, }, () => 0, ); for (const [h, a] of allGamesShuffled) { - ++matchesByTeam[h]; - ++matchesByTeam[a]; + ++numMatchesByTeam[h]; + ++numMatchesByTeam[a]; } - const orderedGames: typeof allGamesShuffled = []; while (allGamesShuffled.length > 0) { - const min = Math.min(...matchesByTeam.filter(item => item > 0)); + const min = Math.min(...numMatchesByTeam.filter(item => item > 0)); const minIndices: number[] = []; - for (const [team, element] of matchesByTeam.entries()) { + for (const [team, element] of numMatchesByTeam.entries()) { if (element === min) { minIndices.push(team); } @@ -51,13 +74,14 @@ export default ({ m => m[0] === minTeam || m[1] === minTeam, ); for (const m of minTeamMatches) { - --matchesByTeam[m[0]]; - --matchesByTeam[m[1]]; + --numMatchesByTeam[m[0]]; + --numMatchesByTeam[m[1]]; } orderedGames.push(...minTeamMatches); } return { + teams, matchdaySize, allGames: orderedGames, currentSchedule, diff --git a/src/engine/dfs/ls/generateSchedule/index.ts b/src/engine/dfs/ls/generateSchedule/index.ts index 47f6c569..84ed2878 100644 --- a/src/engine/dfs/ls/generateSchedule/index.ts +++ b/src/engine/dfs/ls/generateSchedule/index.ts @@ -11,9 +11,10 @@ export default async function* generateSchedule({ allGames: readonly (readonly [T, T])[]; currentSchedule: readonly (readonly (readonly [T, T])[])[]; }) { - const allTeams = allGamesWithIds.flat(); - const teamById = keyBy(allTeams, team => team.id); - const allTeamIds = uniq(allTeams.map(team => team.id)); + const foo = allGamesWithIds.flat(); + const teamById = keyBy(foo, team => team.id); + const allTeamIds = uniq(foo.map(team => team.id)); + const allTeams = allTeamIds.map(id => foo.find(item => item.id === id)!); const indexByTeamId = new Map(allTeamIds.map((id, i) => [id, i] as const)); const currentSchedule: Record<`${number}:${number}`, number> = {}; @@ -40,6 +41,8 @@ export default async function* generateSchedule({ for (const [i, match] of allGamesUnordered.entries()) { // eslint-disable-next-line no-await-in-loop const result = await getFirstSuitableMatchday({ + // @ts-expect-error Fix this later + teams: allTeams, matchdaySize, allGames: allGamesUnordered, currentSchedule, diff --git a/src/engine/dfs/ls/generateSchedule/teamsSharingStadium.ts b/src/engine/dfs/ls/generateSchedule/teamsSharingStadium.ts new file mode 100644 index 00000000..93a53c01 --- /dev/null +++ b/src/engine/dfs/ls/generateSchedule/teamsSharingStadium.ts @@ -0,0 +1 @@ +export default [['Internazionale', 'Milan']] as const; From 485beb002168cef7ace4a2e3302cbc1ad24302b7 Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Mon, 1 Jul 2024 22:51:25 +0100 Subject: [PATCH 28/41] draw in order --- .../dfs/ls/generatePairings/getFirstSuitableMatch.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.ts b/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.ts index ff5e24be..bad20e45 100644 --- a/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.ts +++ b/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.ts @@ -91,10 +91,9 @@ export default ({ console.log('num remaining possible games', remainingGames.length); - const orderedRemainingGames = orderBy( - remainingGames, - (_, i) => randomArray[i], - ); + let orderedRemainingGames = orderBy(remainingGames, (_, i) => randomArray[i]); + + orderedRemainingGames = orderBy(orderedRemainingGames, m => Math.min(...m)); const shuffledRemainingGames = shouldShuffle ? shuffle(remainingGames) From df6a0658a1525c578f06a14bc5ffabf5267b704f Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Mon, 1 Jul 2024 23:43:25 +0100 Subject: [PATCH 29/41] roma & lazio --- src/engine/dfs/ls/generateSchedule/teamsSharingStadium.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/engine/dfs/ls/generateSchedule/teamsSharingStadium.ts b/src/engine/dfs/ls/generateSchedule/teamsSharingStadium.ts index 93a53c01..fd70e944 100644 --- a/src/engine/dfs/ls/generateSchedule/teamsSharingStadium.ts +++ b/src/engine/dfs/ls/generateSchedule/teamsSharingStadium.ts @@ -1 +1,4 @@ -export default [['Internazionale', 'Milan']] as const; +export default [ + ['Internazionale', 'Milan'], + ['Roma', 'Lazio'], +] as const; From db6ad3974e6337b5fb5cf3d5fc136a470a5916fc Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Tue, 2 Jul 2024 01:47:02 +0100 Subject: [PATCH 30/41] abort signal --- .../getFirstSuitableMatch.wrapper.ts | 10 +++++++--- src/engine/dfs/ls/generatePairings/index.ts | 3 +++ .../getFirstSuitableMatchday.wrapper.ts | 3 +++ src/engine/dfs/ls/generateSchedule/index.ts | 3 +++ src/pages/cl/ls/index.tsx | 11 +++++++++++ src/utils/raceWorkers.ts | 18 ++++++++++++++++++ 6 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.wrapper.ts b/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.wrapper.ts index fe650ddc..3f5e0943 100644 --- a/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.wrapper.ts +++ b/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.wrapper.ts @@ -4,9 +4,12 @@ import { type Func } from './getFirstSuitableMatch.worker'; const NUM_WORKERS = Math.max(1, navigator.hardwareConcurrency - 1); -export default async ( - options: Omit[0], 'randomArray' | 'shouldShuffle'>, -) => +export default async ({ + signal, + ...options +}: Omit[0], 'randomArray' | 'shouldShuffle'> & { + signal?: AbortSignal; +}) => raceWorkers({ numWorkers: NUM_WORKERS, getWorker: () => @@ -29,4 +32,5 @@ export default async ( const factor = 7 / (workerIndex + 1); return factor * Math.min(5000, 1000 * Math.exp(iteration / 10)); }, + signal, }); diff --git a/src/engine/dfs/ls/generatePairings/index.ts b/src/engine/dfs/ls/generatePairings/index.ts index e55bb43c..57114284 100644 --- a/src/engine/dfs/ls/generatePairings/index.ts +++ b/src/engine/dfs/ls/generatePairings/index.ts @@ -7,10 +7,12 @@ export default async function* generatePairings({ pots, numMatchdays, isMatchPossible, + signal, }: { pots: readonly (readonly T[])[]; numMatchdays: number; isMatchPossible: (h: T, a: T) => boolean; + signal?: AbortSignal, }) { const teams = pots.flat(); const numTeamsPerPot = pots[0].length; @@ -57,6 +59,7 @@ export default async function* generatePairings({ numGamesPerMatchday, allGames, pickedMatches: matches, + signal, }); matches.push(pickedMatch); diff --git a/src/engine/dfs/ls/generateSchedule/getFirstSuitableMatchday.wrapper.ts b/src/engine/dfs/ls/generateSchedule/getFirstSuitableMatchday.wrapper.ts index 4d8b248c..ed2db647 100644 --- a/src/engine/dfs/ls/generateSchedule/getFirstSuitableMatchday.wrapper.ts +++ b/src/engine/dfs/ls/generateSchedule/getFirstSuitableMatchday.wrapper.ts @@ -17,12 +17,14 @@ export default ({ allGames, currentSchedule, matchIndex, + signal, }: { teams: readonly Team[]; matchdaySize: number; allGames: readonly (readonly [number, number])[]; currentSchedule: Record<`${number}:${number}`, number>; matchIndex: number; + signal: AbortSignal, }) => raceWorkers({ numWorkers: NUM_WORKERS, @@ -92,4 +94,5 @@ export default ({ const factor = 7 / (workerIndex + 1); return factor * Math.min(10000, 5000 * Math.exp(attempt / 10)); }, + signal, }); diff --git a/src/engine/dfs/ls/generateSchedule/index.ts b/src/engine/dfs/ls/generateSchedule/index.ts index 84ed2878..d9611b85 100644 --- a/src/engine/dfs/ls/generateSchedule/index.ts +++ b/src/engine/dfs/ls/generateSchedule/index.ts @@ -6,10 +6,12 @@ export default async function* generateSchedule({ matchdaySize, allGames: allGamesWithIds, currentSchedule: foobar, + signal, }: { matchdaySize: number; allGames: readonly (readonly [T, T])[]; currentSchedule: readonly (readonly (readonly [T, T])[])[]; + signal?: AbortSignal; }) { const foo = allGamesWithIds.flat(); const teamById = keyBy(foo, team => team.id); @@ -47,6 +49,7 @@ export default async function* generateSchedule({ allGames: allGamesUnordered, currentSchedule, matchIndex: i, + signal, }); console.log('for match', match, 'picked', result.pickedMatchday); currentSchedule[`${match[0]}:${match[1]}`] = result.pickedMatchday; diff --git a/src/pages/cl/ls/index.tsx b/src/pages/cl/ls/index.tsx index 146082f6..4f312204 100644 --- a/src/pages/cl/ls/index.tsx +++ b/src/pages/cl/ls/index.tsx @@ -46,6 +46,15 @@ function LeagueStage({ pots: initialPots }: Props) { ); const [isFixturesDone, setIsFixturesDone] = useState(false); + const abortController = useMemo(() => new AbortController(), []); + + // eslint-disable-next-line arrow-body-style + useEffect(() => { + return () => { + abortController.abort(); + }; + }, []); + const pots = useMemo( () => initialPots.map(pot => @@ -73,6 +82,7 @@ function LeagueStage({ pots: initialPots }: Props) { pots, numMatchdays: 8, isMatchPossible: (a, b) => a.country !== b.country, + signal: abortController.signal, }); for await (const pickedMatch of generator) { setPairings(prev => [...prev, pickedMatch]); @@ -93,6 +103,7 @@ function LeagueStage({ pots: initialPots }: Props) { matchdaySize, allGames: pairings, currentSchedule: schedule, + signal, }); const iterator = await generator.next(); if (iterator.done) { diff --git a/src/utils/raceWorkers.ts b/src/utils/raceWorkers.ts index 79055a73..7577ccf2 100644 --- a/src/utils/raceWorkers.ts +++ b/src/utils/raceWorkers.ts @@ -7,14 +7,32 @@ export default async void>({ getWorker, getPayload, getTimeout, + signal, }: { numWorkers: number; getWorker: () => Worker; getPayload: (workerIndex: number, attempt: number) => Parameters[0]; getTimeout: (workerIndex: number, attempt: number) => number; + signal?: AbortSignal; }): Promise>> => { const workers: Worker[] = []; let gotResult = false; + + if (signal) { + signal.addEventListener( + 'abort', + () => { + gotResult = true; + for (const w of workers) { + w.terminate(); + } + }, + { + once: true, + }, + ); + } + const promises = Array.from( { length: numWorkers, From 669623f70f0fb1ef4f5daf08c253e0f6b69ea9bf Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Tue, 2 Jul 2024 12:43:53 +0100 Subject: [PATCH 31/41] fix --- src/pages/cl/ls/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/cl/ls/index.tsx b/src/pages/cl/ls/index.tsx index 4f312204..8590ae12 100644 --- a/src/pages/cl/ls/index.tsx +++ b/src/pages/cl/ls/index.tsx @@ -103,7 +103,7 @@ function LeagueStage({ pots: initialPots }: Props) { matchdaySize, allGames: pairings, currentSchedule: schedule, - signal, + signal: abortController.signal, }); const iterator = await generator.next(); if (iterator.done) { From ab1f3653b09b21409b86f68884203a20de5f9708 Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Tue, 2 Jul 2024 12:44:06 +0100 Subject: [PATCH 32/41] 100 --- .../dfs/ls/generatePairings/getFirstSuitableMatch.wrapper.ts | 2 +- .../dfs/ls/generateSchedule/getFirstSuitableMatchday.wrapper.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.wrapper.ts b/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.wrapper.ts index 3f5e0943..f83e2f40 100644 --- a/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.wrapper.ts +++ b/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.wrapper.ts @@ -29,7 +29,7 @@ export default async ({ }; }, getTimeout: (workerIndex, iteration) => { - const factor = 7 / (workerIndex + 1); + const factor = workerIndex === 0 ? 100 : 7 / (workerIndex + 1); return factor * Math.min(5000, 1000 * Math.exp(iteration / 10)); }, signal, diff --git a/src/engine/dfs/ls/generateSchedule/getFirstSuitableMatchday.wrapper.ts b/src/engine/dfs/ls/generateSchedule/getFirstSuitableMatchday.wrapper.ts index ed2db647..4aed79d9 100644 --- a/src/engine/dfs/ls/generateSchedule/getFirstSuitableMatchday.wrapper.ts +++ b/src/engine/dfs/ls/generateSchedule/getFirstSuitableMatchday.wrapper.ts @@ -91,7 +91,7 @@ export default ({ }; }, getTimeout: (workerIndex, attempt) => { - const factor = 7 / (workerIndex + 1); + const factor = workerIndex === 0 ? 100 : 7 / (workerIndex + 1); return factor * Math.min(10000, 5000 * Math.exp(attempt / 10)); }, signal, From 796c7cb73c89e0118ca27926432525be18842b47 Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Tue, 2 Jul 2024 16:09:42 +0100 Subject: [PATCH 33/41] useAbortSignal --- src/pages/cl/ls/index.tsx | 14 ++++---------- src/utils/hooks/useAbortSignal.ts | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 src/utils/hooks/useAbortSignal.ts diff --git a/src/pages/cl/ls/index.tsx b/src/pages/cl/ls/index.tsx index 8590ae12..8524ecd1 100644 --- a/src/pages/cl/ls/index.tsx +++ b/src/pages/cl/ls/index.tsx @@ -6,6 +6,7 @@ import usePopup from '#store/usePopup'; import type Team from '#model/team/GsTeam'; import generatePairings from '#engine/dfs/ls/generatePairings/index'; import generateSchedule from '#engine/dfs/ls/generateSchedule/index'; +import useAbortSignal from '#utils/hooks/useAbortSignal'; import Button from '#ui/Button'; import Portal from '#ui/Portal'; @@ -46,14 +47,7 @@ function LeagueStage({ pots: initialPots }: Props) { ); const [isFixturesDone, setIsFixturesDone] = useState(false); - const abortController = useMemo(() => new AbortController(), []); - - // eslint-disable-next-line arrow-body-style - useEffect(() => { - return () => { - abortController.abort(); - }; - }, []); + const abortSignal = useAbortSignal(); const pots = useMemo( () => @@ -82,7 +76,7 @@ function LeagueStage({ pots: initialPots }: Props) { pots, numMatchdays: 8, isMatchPossible: (a, b) => a.country !== b.country, - signal: abortController.signal, + signal: abortSignal, }); for await (const pickedMatch of generator) { setPairings(prev => [...prev, pickedMatch]); @@ -103,7 +97,7 @@ function LeagueStage({ pots: initialPots }: Props) { matchdaySize, allGames: pairings, currentSchedule: schedule, - signal: abortController.signal, + signal: abortSignal, }); const iterator = await generator.next(); if (iterator.done) { diff --git a/src/utils/hooks/useAbortSignal.ts b/src/utils/hooks/useAbortSignal.ts new file mode 100644 index 00000000..deabd13d --- /dev/null +++ b/src/utils/hooks/useAbortSignal.ts @@ -0,0 +1,14 @@ +import { useEffect, useMemo } from 'react'; + +export default () => { + const abortController = useMemo(() => new AbortController(), []); + + // eslint-disable-next-line arrow-body-style + useEffect(() => { + return () => { + abortController.abort(); + }; + }, []); + + return abortController.signal; +}; From 7f753eeb9cecbf40f6d0302d973da9eb72993f6c Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Tue, 2 Jul 2024 16:27:21 +0100 Subject: [PATCH 34/41] upd --- .../getFirstSuitableMatchday.wrapper.ts | 8 +++----- src/engine/dfs/ls/generateSchedule/index.ts | 19 ++++++++++--------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/engine/dfs/ls/generateSchedule/getFirstSuitableMatchday.wrapper.ts b/src/engine/dfs/ls/generateSchedule/getFirstSuitableMatchday.wrapper.ts index 4aed79d9..a2eccba7 100644 --- a/src/engine/dfs/ls/generateSchedule/getFirstSuitableMatchday.wrapper.ts +++ b/src/engine/dfs/ls/generateSchedule/getFirstSuitableMatchday.wrapper.ts @@ -24,7 +24,7 @@ export default ({ allGames: readonly (readonly [number, number])[]; currentSchedule: Record<`${number}:${number}`, number>; matchIndex: number; - signal: AbortSignal, + signal?: AbortSignal; }) => raceWorkers({ numWorkers: NUM_WORKERS, @@ -40,16 +40,14 @@ export default ({ const orderedGames: typeof allGamesShuffled = []; for (const team of prioritizedTeams) { - const hah = remove(allGamesShuffled, m => { + const prioritizedGames = remove(allGamesShuffled, m => { const h = teams[m[0]]; const a = teams[m[1]]; return team === h || team === a; }); - orderedGames.push(...hah); + orderedGames.push(...prioritizedGames); } - console.log('remaining after priority', allGamesShuffled); - const numMatchesByTeam = Array.from( { length: teams.length, diff --git a/src/engine/dfs/ls/generateSchedule/index.ts b/src/engine/dfs/ls/generateSchedule/index.ts index d9611b85..61296642 100644 --- a/src/engine/dfs/ls/generateSchedule/index.ts +++ b/src/engine/dfs/ls/generateSchedule/index.ts @@ -5,7 +5,7 @@ import getFirstSuitableMatchday from './getFirstSuitableMatchday.wrapper'; export default async function* generateSchedule({ matchdaySize, allGames: allGamesWithIds, - currentSchedule: foobar, + currentSchedule: currentScheduleWithIds, signal, }: { matchdaySize: number; @@ -13,14 +13,16 @@ export default async function* generateSchedule({ currentSchedule: readonly (readonly (readonly [T, T])[])[]; signal?: AbortSignal; }) { - const foo = allGamesWithIds.flat(); - const teamById = keyBy(foo, team => team.id); - const allTeamIds = uniq(foo.map(team => team.id)); - const allTeams = allTeamIds.map(id => foo.find(item => item.id === id)!); + const allNonUniqueTeams = allGamesWithIds.flat(); + const teamById = keyBy(allNonUniqueTeams, team => team.id); + const allTeamIds = uniq(allNonUniqueTeams.map(team => team.id)); + const allTeams = allTeamIds.map( + id => allNonUniqueTeams.find(item => item.id === id)!, + ); const indexByTeamId = new Map(allTeamIds.map((id, i) => [id, i] as const)); const currentSchedule: Record<`${number}:${number}`, number> = {}; - for (const [matchdayIndex, matchday] of foobar.entries()) { + for (const [matchdayIndex, matchday] of currentScheduleWithIds.entries()) { for (const [h, a] of matchday) { const homeIndex = indexByTeamId.get(h.id)!; const awayIndex = indexByTeamId.get(a.id)!; @@ -51,7 +53,6 @@ export default async function* generateSchedule({ matchIndex: i, signal, }); - console.log('for match', match, 'picked', result.pickedMatchday); currentSchedule[`${match[0]}:${match[1]}`] = result.pickedMatchday; const homeTeam = teamById[allTeamIds[match[0]]]; @@ -60,7 +61,7 @@ export default async function* generateSchedule({ m => m[0].id === homeTeam.id && m[1].id === awayTeam.id, )!; - const haha = result.matchdays.map(md => + const solutionSchedule = result.matchdays.map(md => md.map(([h, a]) => { const ht = teamById[allTeamIds[h]]; const at = teamById[allTeamIds[a]]; @@ -73,7 +74,7 @@ export default async function* generateSchedule({ yield { match: originalMatch, matchday: result.pickedMatchday, - solutionSchedule: haha, + solutionSchedule, }; } } From 139b05a97faf8c150d79a20289edeebc141b1b6e Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Tue, 2 Jul 2024 22:08:13 +0100 Subject: [PATCH 35/41] remove shouldShuffle --- src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.ts | 6 +----- .../ls/generatePairings/getFirstSuitableMatch.wrapper.ts | 6 ++---- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.ts b/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.ts index bad20e45..167040a0 100644 --- a/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.ts +++ b/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.ts @@ -16,7 +16,6 @@ export default ({ allGames, pickedMatches, randomArray, - shouldShuffle, }: { teams: readonly Team[]; numPots: number; @@ -26,7 +25,6 @@ export default ({ allGames: readonly (readonly [number, number])[]; pickedMatches: readonly (readonly [number, number])[]; randomArray: readonly number[]; - shouldShuffle: boolean; }) => { const maxGamesAtHome = Math.ceil(numMatchdays / 2); @@ -95,9 +93,7 @@ export default ({ orderedRemainingGames = orderBy(orderedRemainingGames, m => Math.min(...m)); - const shuffledRemainingGames = shouldShuffle - ? shuffle(remainingGames) - : remainingGames; + const shuffledRemainingGames = shuffle(remainingGames); return orderedRemainingGames.find(m => { console.log('test...', m); diff --git a/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.wrapper.ts b/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.wrapper.ts index f83e2f40..a7f348aa 100644 --- a/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.wrapper.ts +++ b/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.wrapper.ts @@ -7,25 +7,23 @@ const NUM_WORKERS = Math.max(1, navigator.hardwareConcurrency - 1); export default async ({ signal, ...options -}: Omit[0], 'randomArray' | 'shouldShuffle'> & { +}: Omit[0], 'randomArray'> & { signal?: AbortSignal; }) => raceWorkers({ numWorkers: NUM_WORKERS, getWorker: () => new Worker(new URL('./getFirstSuitableMatch.worker', import.meta.url)), - getPayload: (workerIndex, attempt) => { + getPayload: () => { const randomArray = Array.from( { length: options.allGames.length, }, () => Math.random(), ); - const shouldNotShuffle = workerIndex === 0 && attempt === 0; return { ...options, randomArray, - shouldShuffle: !shouldNotShuffle, }; }, getTimeout: (workerIndex, iteration) => { From 1dea50d38cc89df7e277408766f79035976d55cb Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Tue, 2 Jul 2024 22:31:37 +0100 Subject: [PATCH 36/41] upd --- src/engine/dfs/ls/generatePairings/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/dfs/ls/generatePairings/index.ts b/src/engine/dfs/ls/generatePairings/index.ts index 57114284..efbcfd38 100644 --- a/src/engine/dfs/ls/generatePairings/index.ts +++ b/src/engine/dfs/ls/generatePairings/index.ts @@ -12,7 +12,7 @@ export default async function* generatePairings({ pots: readonly (readonly T[])[]; numMatchdays: number; isMatchPossible: (h: T, a: T) => boolean; - signal?: AbortSignal, + signal?: AbortSignal; }) { const teams = pots.flat(); const numTeamsPerPot = pots[0].length; From c8f2971ed7c52fe14134a9adea90926b59f10787 Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Wed, 3 Jul 2024 02:18:34 +0100 Subject: [PATCH 37/41] fix --- src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.ts b/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.ts index 167040a0..e09286da 100644 --- a/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.ts +++ b/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.ts @@ -196,11 +196,11 @@ export default ({ const homePot = Math.floor(h / numTeamsPerPot); const awayPot = Math.floor(a / numTeamsPerPot); - if (hasPlayedWithPotMap[`${h}:${awayPot}:h`]) { + if (newHasPlayedWithPotMap[`${h}:${awayPot}:h`]) { return false; } - if (hasPlayedWithPotMap[`${a}:${homePot}:a`]) { + if (newHasPlayedWithPotMap[`${a}:${homePot}:a`]) { return false; } From d2e2492e26151e7286b11132428f9c78d759bf2c Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Wed, 3 Jul 2024 19:26:09 +0100 Subject: [PATCH 38/41] pot pairs --- .../generatePairings/getFirstSuitableMatch.ts | 99 ++++++++----------- .../getFirstSuitableMatch.wrapper.ts | 55 ++++++----- src/utils/cartesian.ts | 9 ++ 3 files changed, 80 insertions(+), 83 deletions(-) create mode 100644 src/utils/cartesian.ts diff --git a/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.ts b/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.ts index e09286da..220321e5 100644 --- a/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.ts +++ b/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.ts @@ -1,7 +1,8 @@ -import { orderBy, shuffle } from 'lodash'; +import { orderBy, range } from 'lodash'; import { findFirstSolution } from '#utils/backtrack'; import { type Country } from '#model/types'; +import cartesian from '#utils/cartesian'; interface Team { country: Country; @@ -15,7 +16,6 @@ export default ({ numGamesPerMatchday, allGames, pickedMatches, - randomArray, }: { teams: readonly Team[]; numPots: number; @@ -24,8 +24,8 @@ export default ({ numGamesPerMatchday: number; allGames: readonly (readonly [number, number])[]; pickedMatches: readonly (readonly [number, number])[]; - randomArray: readonly number[]; }) => { + const potIndices = range(numPots); const maxGamesAtHome = Math.ceil(numMatchdays / 2); const numHomeGamesByTeam: Record = {}; @@ -40,6 +40,13 @@ export default ({ boolean > = {}; + const potPairs = orderBy(cartesian(potIndices, potIndices), ([h, a]) => { + if (h === a) { + return h / 1000; + } + return h + a * 0.0001 + 1000; + }); + for (const m of pickedMatches) { const homeTeam = teams[m[0]]; const awayTeam = teams[m[1]]; @@ -89,17 +96,20 @@ export default ({ console.log('num remaining possible games', remainingGames.length); - let orderedRemainingGames = orderBy(remainingGames, (_, i) => randomArray[i]); - - orderedRemainingGames = orderBy(orderedRemainingGames, m => Math.min(...m)); - - const shuffledRemainingGames = shuffle(remainingGames); + const orderedRemainingGames = orderBy(remainingGames, [ + m => { + const hPot = Math.floor(m[0] / numTeamsPerPot); + const aPot = Math.floor(m[1] / numTeamsPerPot); + return potPairs.findIndex(([a, b]) => a === hPot && b === aPot); + }, + m => m[0], + ]); return orderedRemainingGames.find(m => { console.log('test...', m); const solution = findFirstSolution( { - source: shuffledRemainingGames, + source: remainingGames, target: pickedMatches, numHomeGamesByTeam, numAwayGamesByTeam, @@ -207,55 +217,32 @@ export default ({ return true; }); - const candidates: (typeof c)[] = []; + const currentPotPairIndex = Math.floor( + newTarget.length / numTeamsPerPot, + ); + const [potPairHomePot, potPairAwayPot] = + potPairs[currentPotPairIndex]; + const newHomeTeam = + numTeamsPerPot * potPairHomePot + + (newTarget.length % numTeamsPerPot); - const lowestRemainingTeam = - newSource.length > 0 - ? // eslint-disable-next-line unicorn/no-array-reduce - newSource.reduce( - (prev, cur) => Math.min(prev, ...cur), - Math.min(...newSource[0]), - ) - : undefined; - - if (lowestRemainingTeam !== undefined) { - let nextPot: number | undefined; - let nextPlace: 'h' | 'a' | undefined; - for (let i = 0; i < numPots; ++i) { - if (!newHasPlayedWithPotMap[`${lowestRemainingTeam}:${i}:h`]) { - nextPot = i; - nextPlace = 'h'; - break; - } - if (!newHasPlayedWithPotMap[`${lowestRemainingTeam}:${i}:a`]) { - nextPot = i; - nextPlace = 'a'; - break; - } - } + const candidates: (typeof c)[] = []; - for (const newPicked of newSource) { - const homePot = Math.floor(newPicked[0] / numTeamsPerPot); - const awayPot = Math.floor(newPicked[1] / numTeamsPerPot); - - const isMatchGood = - (nextPlace === 'h' && - newPicked[0] === lowestRemainingTeam && - awayPot === nextPot) || - (nextPlace === 'a' && - newPicked[1] === lowestRemainingTeam && - homePot === nextPot); - if (isMatchGood) { - candidates.push({ - source: newSource, - target: newTarget, - picked: newPicked, - numHomeGamesByTeam: newNumHomeGamesByTeam, - numAwayGamesByTeam: newNumAwayGamesByTeam, - numOpponentCountriesByTeam: newNumOpponentCountriesByTeam, - hasPlayedWithPotMap: newHasPlayedWithPotMap, - }); - } + for (const newPicked of newSource) { + const awayPot = Math.floor(newPicked[1] / numTeamsPerPot); + + const isMatchGood = + newPicked[0] === newHomeTeam && awayPot === potPairAwayPot; + if (isMatchGood) { + candidates.push({ + source: newSource, + target: newTarget, + picked: newPicked, + numHomeGamesByTeam: newNumHomeGamesByTeam, + numAwayGamesByTeam: newNumAwayGamesByTeam, + numOpponentCountriesByTeam: newNumOpponentCountriesByTeam, + hasPlayedWithPotMap: newHasPlayedWithPotMap, + }); } } diff --git a/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.wrapper.ts b/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.wrapper.ts index a7f348aa..025951ce 100644 --- a/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.wrapper.ts +++ b/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.wrapper.ts @@ -1,34 +1,35 @@ -import raceWorkers from '#utils/raceWorkers'; +import workerSendAndReceive from '#utils/worker/sendAndReceive'; import { type Func } from './getFirstSuitableMatch.worker'; -const NUM_WORKERS = Math.max(1, navigator.hardwareConcurrency - 1); - export default async ({ signal, ...options -}: Omit[0], 'randomArray'> & { +}: Parameters[0] & { signal?: AbortSignal; -}) => - raceWorkers({ - numWorkers: NUM_WORKERS, - getWorker: () => - new Worker(new URL('./getFirstSuitableMatch.worker', import.meta.url)), - getPayload: () => { - const randomArray = Array.from( - { - length: options.allGames.length, - }, - () => Math.random(), - ); - return { - ...options, - randomArray, - }; - }, - getTimeout: (workerIndex, iteration) => { - const factor = workerIndex === 0 ? 100 : 7 / (workerIndex + 1); - return factor * Math.min(5000, 1000 * Math.exp(iteration / 10)); - }, - signal, - }); +}) => { + const worker = new Worker( + new URL('./getFirstSuitableMatch.worker', import.meta.url), + ); + + if (signal) { + signal.addEventListener( + 'abort', + () => { + worker.terminate(); + }, + { + once: true, + }, + ); + } + + try { + const invoke = workerSendAndReceive[0], ReturnType>( + worker, + ); + return await invoke(options); + } finally { + worker.terminate(); + } +}; diff --git a/src/utils/cartesian.ts b/src/utils/cartesian.ts new file mode 100644 index 00000000..058015aa --- /dev/null +++ b/src/utils/cartesian.ts @@ -0,0 +1,9 @@ +export default (a: Iterable, b: Iterable) => { + const result: [A, B][] = []; + for (const i of a) { + for (const j of b) { + result.push([i, j]); + } + } + return result; +}; From 1430fca7eb9ea39a0adbbf068bda6f9b5048a308 Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Wed, 3 Jul 2024 19:27:30 +0100 Subject: [PATCH 39/41] shuffle on every iteration --- src/engine/dfs/ls/generatePairings/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/engine/dfs/ls/generatePairings/index.ts b/src/engine/dfs/ls/generatePairings/index.ts index efbcfd38..d2dc5745 100644 --- a/src/engine/dfs/ls/generatePairings/index.ts +++ b/src/engine/dfs/ls/generatePairings/index.ts @@ -42,13 +42,12 @@ export default async function* generatePairings({ allGames = allGames.filter(([h, a]) => isMatchPossible(teams[h], teams[a])); - allGames = shuffle(allGames); - console.log('initial games', allGames.length, JSON.stringify(allGames)); const matches: (readonly [number, number])[] = []; while (matches.length < numMatchdays * numGamesPerMatchday) { + allGames = shuffle(allGames); // eslint-disable-next-line no-await-in-loop const pickedMatch = await getFirstSuitableMatch({ // @ts-expect-error Fix this later From 7e2eb0a4665a14a56d22a6032fccb19b13876efa Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Wed, 3 Jul 2024 19:31:45 +0100 Subject: [PATCH 40/41] fix lint --- .../dfs/ls/generatePairings/getFirstSuitableMatch.ts | 3 --- src/engine/dfs/ls/generatePairings/index.ts | 2 -- .../dfs/ls/generateSchedule/getFirstSuitableMatchday.ts | 6 +----- src/pages/cl/ls/index.tsx | 1 - src/utils/raceWorkers.ts | 8 -------- 5 files changed, 1 insertion(+), 19 deletions(-) diff --git a/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.ts b/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.ts index 220321e5..5c8b808d 100644 --- a/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.ts +++ b/src/engine/dfs/ls/generatePairings/getFirstSuitableMatch.ts @@ -94,8 +94,6 @@ export default ({ return true; }); - console.log('num remaining possible games', remainingGames.length); - const orderedRemainingGames = orderBy(remainingGames, [ m => { const hPot = Math.floor(m[0] / numTeamsPerPot); @@ -106,7 +104,6 @@ export default ({ ]); return orderedRemainingGames.find(m => { - console.log('test...', m); const solution = findFirstSolution( { source: remainingGames, diff --git a/src/engine/dfs/ls/generatePairings/index.ts b/src/engine/dfs/ls/generatePairings/index.ts index d2dc5745..2aaf2aa9 100644 --- a/src/engine/dfs/ls/generatePairings/index.ts +++ b/src/engine/dfs/ls/generatePairings/index.ts @@ -42,8 +42,6 @@ export default async function* generatePairings({ allGames = allGames.filter(([h, a]) => isMatchPossible(teams[h], teams[a])); - console.log('initial games', allGames.length, JSON.stringify(allGames)); - const matches: (readonly [number, number])[] = []; while (matches.length < numMatchdays * numGamesPerMatchday) { diff --git a/src/engine/dfs/ls/generateSchedule/getFirstSuitableMatchday.ts b/src/engine/dfs/ls/generateSchedule/getFirstSuitableMatchday.ts index 51e25a19..ffa75a38 100644 --- a/src/engine/dfs/ls/generateSchedule/getFirstSuitableMatchday.ts +++ b/src/engine/dfs/ls/generateSchedule/getFirstSuitableMatchday.ts @@ -191,9 +191,8 @@ export default ({ c.numMatchesPerMatchday[c.pickedMatchday] + 1, ); - // console.log(newMatchIndex); - if (newMatchIndex > record) { + // eslint-disable-next-line no-console console.log(newMatchIndex); record = newMatchIndex; } @@ -218,9 +217,6 @@ export default ({ }, ); - // if (!solution) { - // console.log('sol', solution); - // } if (solution) { const arr = Array.from( { diff --git a/src/pages/cl/ls/index.tsx b/src/pages/cl/ls/index.tsx index 8524ecd1..668f11b4 100644 --- a/src/pages/cl/ls/index.tsx +++ b/src/pages/cl/ls/index.tsx @@ -81,7 +81,6 @@ function LeagueStage({ pots: initialPots }: Props) { for await (const pickedMatch of generator) { setPairings(prev => [...prev, pickedMatch]); } - console.log('pairings', JSON.stringify(pairings)); setIsFixturesDone(true); }; diff --git a/src/utils/raceWorkers.ts b/src/utils/raceWorkers.ts index 7577ccf2..3331b8f8 100644 --- a/src/utils/raceWorkers.ts +++ b/src/utils/raceWorkers.ts @@ -39,14 +39,6 @@ export default async void>({ }, async (_, workerIndex) => { for (let attempt = 0; !gotResult; ++attempt) { - if (workerIndex === 0) { - console.log( - 'spawning', - workerIndex, - attempt, - getTimeout(workerIndex, attempt), - ); - } const worker = getWorker(); workers[workerIndex] = worker; // eslint-disable-next-line no-await-in-loop From bd34aa3071e85a9a8407c1c0a352d1440c4f819f Mon Sep 17 00:00:00 2001 From: Anton Veselev Date: Wed, 3 Jul 2024 19:34:20 +0100 Subject: [PATCH 41/41] remove leftovers --- src/index.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 9d871b65..09476fec 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -6,7 +6,3 @@ import App from './App'; const container = document.getElementById('app')!; const root = createRoot(container); root.render(); - -setTimeout(() => { - // import('./experiments'); -}, 2000);