import playersData from "@/data/players.json"; import puzzlesData from "@/data/puzzles.json"; import { getDateKeyInTimeZone } from "./time"; import type { Difficulty, Feedback, Guess, Player, Position, Puzzle } from "./types"; export const POSITIONS: Position[] = ["GK", "DEF", "MID", "WING", "ST"]; export const DIFFICULTIES: Difficulty[] = ["easy", "medium", "hard"]; export const MAX_ATTEMPTS = 6; export const players = playersData as Player[]; export const puzzles = puzzlesData as Puzzle[]; export function getTodayKey(date = new Date()): string { return getDateKeyInTimeZone(date); } export function getPuzzleNumber(dateKey: string): number { const epoch = Date.UTC(2026, 0, 1); const current = Date.parse(`${dateKey}T00:00:00.000Z`); return Math.max(1, Math.floor((current - epoch) / 86400000) + 1); } export function getDateKeyForPuzzleNumber(puzzleNumber: number): string { const epoch = Date.UTC(2026, 0, 1); return new Date(epoch + (puzzleNumber - 1) * 86400000).toISOString().slice(0, 10); } // Past days that have a dedicated (non-rotated) puzzle set — the only ones the // public archive exposes, so answer pages never duplicate or leak content. export function getArchiveDateKeys(todayKey: string): string[] { const complete = new Map(); puzzles.forEach((puzzle) => { complete.set(puzzle.date, (complete.get(puzzle.date) ?? 0) + 1); }); return Array.from(complete.entries()) .filter(([date, count]) => count === DIFFICULTIES.length && date < todayKey) .map(([date]) => date) .sort((left, right) => right.localeCompare(left)); } export function getDailyPuzzles(dateKey: string): Puzzle[] { const dated = puzzles.filter((puzzle) => puzzle.date === dateKey); const source = dated.length === DIFFICULTIES.length ? dated : getRotatingPuzzleSet(dateKey); return DIFFICULTIES.map((difficulty) => { const puzzle = source.find((item) => item.difficulty === difficulty); if (!puzzle) { throw new Error(`Missing ${difficulty} puzzle`); } return puzzle; }); } function getRotatingPuzzleSet(dateKey: string): Puzzle[] { const grouped = Array.from( puzzles.reduce((map, puzzle) => { const bucket = map.get(puzzle.date) ?? []; bucket.push(puzzle); map.set(puzzle.date, bucket); return map; }, new Map()), ) .map(([, items]) => items) .filter((items) => items.length === DIFFICULTIES.length) .sort((left, right) => left[0].date.localeCompare(right[0].date)); if (grouped.length === 0) { throw new Error("No complete puzzle sets available"); } const index = (getPuzzleNumber(dateKey) - 1) % grouped.length; return grouped[index]; } export function findPlayer(id: string | null): Player | undefined { if (!id) return undefined; return players.find((player) => player.id === id); } export function validateGuess(puzzle: Puzzle, guess: Record): Guess { return { slots: POSITIONS.map((position) => ({ position, playerId: guess[position], feedback: scoreSlot(puzzle.solution[position], guess[position]), })), }; } export function isSolved(guess: Guess): boolean { return guess.slots.every((slot) => slot.feedback === "correct"); } export function isAttemptComplete(guess: Record): boolean { return POSITIONS.every((position) => Boolean(guess[position])); } export function getFixedGuess(puzzle: Puzzle): Record { return POSITIONS.reduce>((acc, position) => { acc[position] = position === puzzle.fixedPosition ? puzzle.solution[position] : null; return acc; }, {} as Record); } function scoreSlot(solutionId: string, guessId: string | null): Feedback { if (!guessId) return "empty"; if (guessId === solutionId) return "correct"; const solution = findPlayer(solutionId); const guess = findPlayer(guessId); if (!solution || !guess) return "wrong"; const hasClubRelation = solution.club === guess.club; const hasLeagueMatch = solution.league === guess.league; const hasNationalityMatch = solution.nationality === guess.nationality; const hasPositionMatch = solution.position === guess.position; return hasClubRelation || hasLeagueMatch || hasNationalityMatch || hasPositionMatch ? "partial" : "wrong"; }