Files
alexandrev-tibco f4157f6b82
Build and push image / build (push) Successful in 1m15s
Improve puzzle difficulty and sharing
2026-05-04 21:04:27 +02:00

103 lines
3.5 KiB
TypeScript

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 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<string, Puzzle[]>()),
)
.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<Position, string | null>): 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<Position, string | null>): boolean {
return POSITIONS.every((position) => Boolean(guess[position]));
}
export function getFixedGuess(puzzle: Puzzle): Record<Position, string | null> {
return POSITIONS.reduce<Record<Position, string | null>>((acc, position) => {
acc[position] = position === puzzle.fixedPosition ? puzzle.solution[position] : null;
return acc;
}, {} as Record<Position, string | null>);
}
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";
}