import { writeFileSync } from "node:fs"; import players from "../data/players.json" with { type: "json" }; const POSITIONS = ["GK", "DEF", "MID", "WING", "ST"]; const DIFFICULTIES = ["easy", "medium", "hard"]; const positionLabels = { GK: "goalkeeper", DEF: "defender", MID: "midfielder", WING: "winger", ST: "striker", }; const START_DATE_KEY = process.env.PUZZLE_START_DATE ?? "2026-05-05"; const DAYS = 365; const byPosition = POSITIONS.map((position) => players.filter((player) => player.position === position)); const totalCombinations = byPosition.reduce((product, group) => product * group.length, 1); const permutationA = 1103; const permutationB = 7919; const puzzles = []; for (let day = 0; day < DAYS; day += 1) { const dateKey = addDays(START_DATE_KEY, day); for (let difficultyIndex = 0; difficultyIndex < DIFFICULTIES.length; difficultyIndex += 1) { const ordinal = day * DIFFICULTIES.length + difficultyIndex; const index = permuteIndex(ordinal, totalCombinations); const lineup = lineupFromIndex(index); const difficulty = DIFFICULTIES[difficultyIndex]; const fixedPosition = POSITIONS[(ordinal + 2) % POSITIONS.length]; puzzles.push({ id: `${dateKey}-${difficulty}`, date: dateKey, difficulty, solution: Object.fromEntries(POSITIONS.map((position) => [position, lineup[position].id])), fixedPosition, clues: buildClues(lineup, difficulty, fixedPosition), }); } } writeFileSync(new URL("../data/puzzles.json", import.meta.url), `${JSON.stringify(puzzles, null, 2)}\n`); function permuteIndex(value, modulo) { return (permutationA * value + permutationB) % modulo; } function lineupFromIndex(index) { const [gks, defs, mids, wings, sts] = byPosition; const divisorGk = defs.length * mids.length * wings.length * sts.length; const divisorDef = mids.length * wings.length * sts.length; const divisorMid = wings.length * sts.length; let remaining = index; const gkIndex = Math.floor(remaining / divisorGk); remaining %= divisorGk; const defIndex = Math.floor(remaining / divisorDef); remaining %= divisorDef; const midIndex = Math.floor(remaining / divisorMid); remaining %= divisorMid; const wingIndex = Math.floor(remaining / sts.length); const stIndex = remaining % sts.length; return { GK: gks[gkIndex], DEF: defs[defIndex], MID: mids[midIndex], WING: wings[wingIndex], ST: sts[stIndex], }; } function buildClues(lineup, difficulty, fixedPosition) { const hiddenPositions = POSITIONS.filter((position) => position !== fixedPosition); if (difficulty === "easy") { return hiddenPositions.flatMap((position) => [ buildNationalityClue(position, lineup[position]), buildCurrentLeagueClue(position, lineup[position]), ]); } if (difficulty === "medium") { return dedupeClues([ ...hiddenPositions.map((position) => buildTitleCountryClue(lineup[position], subjectFor(position))), ...buildHiddenPairClues(lineup, hiddenPositions).slice(0, 2), ]); } return dedupeClues([ ...hiddenPositions.map((position) => buildTitleCountryPairClue(lineup[position], subjectFor(position))), ...hiddenPositions.map((position) => buildChampionsLeagueCountClue(lineup[position], subjectFor(position))), ...buildHiddenPairClues(lineup, hiddenPositions).slice(0, 3), ]); } function subjectFor(position) { return `The ${positionLabels[position]}`; } function buildNationalityClue(position, player) { return `${subjectFor(position)} is from ${player.nationality}.`; } function buildCurrentLeagueClue(position, player) { return `${subjectFor(position)} plays in ${player.league}.`; } function buildTitleCountryClue(player, subject) { const country = player.leagueTitleCountries?.[0]; if (country) { return `${subject} has won league titles in ${country}.`; } return `${subject} has won ${player.leagueTitles} league titles.`; } function buildChampionsLeagueCountClue(player, subject) { return `${subject} has won ${player.championsLeagueTitles} Champions League title${player.championsLeagueTitles === 1 ? "" : "s"}.`; } function buildNumericLeagueClue(player, subject) { return buildLeagueTitleCountClue(player, subject); } function buildFormerClubClue(playerA, playerB, subject) { const common = (playerA.formerClubs ?? []).find((club) => (playerB.formerClubs ?? []).includes(club)); if (common) { return `${subject} were once teammates at ${common}.`; } if ((playerA.championsLeagueTitles ?? 0) === (playerB.championsLeagueTitles ?? 0)) { return `${subject} have won the same number of Champions League titles.`; } if ((playerA.leagueTitles ?? 0) === (playerB.leagueTitles ?? 0)) { return `${subject} have won the same number of league titles.`; } const firstCountry = playerA.leagueTitleCountries?.[0] ?? null; if (firstCountry) { return `${subject.split(" and ")[0]} has won league titles in ${firstCountry}.`; } return buildLeagueTitleCountClue(playerA, subject.split(" and ")[0]); } function buildHiddenPairClues(lineup, hiddenPositions) { const clues = []; for (let leftIndex = 0; leftIndex < hiddenPositions.length; leftIndex += 1) { for (let rightIndex = leftIndex + 1; rightIndex < hiddenPositions.length; rightIndex += 1) { const left = hiddenPositions[leftIndex]; const right = hiddenPositions[rightIndex]; clues.push(buildFormerClubClue(lineup[left], lineup[right], `${subjectFor(left)} and ${positionLabels[right]}`)); clues.push(buildLeagueTitleComparisonClue(left, lineup[left], right, lineup[right])); clues.push(buildChampionsLeagueComparisonClue(left, lineup[left], right, lineup[right])); } } return dedupeClues(clues); } function buildTitleCountryPairClue(player, subject) { const countries = player.leagueTitleCountries ?? []; if (countries.length >= 2) { return `${subject} has won league titles in ${countries.slice(0, 2).join(" and ")}.`; } if (countries.length === 1) { return `${subject} has won league titles in ${countries[0]}.`; } return buildLeagueTitleCountClue(player, subject); } function buildLeagueTitleCountClue(player, subject) { return `${subject} has won ${player.leagueTitles} league title${player.leagueTitles === 1 ? "" : "s"}.`; } function buildLeagueTitleComparisonClue(positionA, playerA, positionB, playerB) { const subjectA = subjectFor(positionA); const subjectB = `the ${positionLabels[positionB]}`; if (playerA.leagueTitles > playerB.leagueTitles) { return `${subjectA} has more league titles than ${subjectB}.`; } if (playerB.leagueTitles > playerA.leagueTitles) { return `${subjectFor(positionB)} has more league titles than the ${positionLabels[positionA]}.`; } return `${subjectA} and ${positionLabels[positionB]} have won the same number of league titles.`; } function buildChampionsLeagueComparisonClue(positionA, playerA, positionB, playerB) { const subjectA = subjectFor(positionA); const subjectB = `the ${positionLabels[positionB]}`; if (playerA.championsLeagueTitles > playerB.championsLeagueTitles) { return `${subjectA} has more Champions League titles than ${subjectB}.`; } if (playerB.championsLeagueTitles > playerA.championsLeagueTitles) { return `${subjectFor(positionB)} has more Champions League titles than the ${positionLabels[positionA]}.`; } return `${subjectA} and ${positionLabels[positionB]} have won the same number of Champions League titles.`; } function dedupeClues(clues) { return [...new Set(clues)]; } function addDays(dateKey, daysToAdd) { const date = new Date(`${dateKey}T00:00:00.000Z`); date.setUTCDate(date.getUTCDate() + daysToAdd); return date.toISOString().slice(0, 10); } function getTodayKeyInTimeZone(date, timeZone) { const formatter = new Intl.DateTimeFormat("en-CA", { timeZone, year: "numeric", month: "2-digit", day: "2-digit", }); const parts = formatter.formatToParts(date); const year = parts.find((part) => part.type === "year")?.value; const month = parts.find((part) => part.type === "month")?.value; const day = parts.find((part) => part.type === "day")?.value; return `${year}-${month}-${day}`; }