Reveal failed puzzles and improve clue coverage
Build and push image / build (push) Successful in 1m19s
Build and push image / build (push) Successful in 1m19s
This commit is contained in:
@@ -56,6 +56,7 @@ export default function Home({ sponsor }: HomeProps) {
|
||||
const [started, setStarted] = useState(false);
|
||||
const [feedbackOpen, setFeedbackOpen] = useState(false);
|
||||
const [statsOpen, setStatsOpen] = useState(false);
|
||||
const [revealedSolutions, setRevealedSolutions] = useState<Partial<Record<Difficulty, boolean>>>({});
|
||||
const [selectedPosition, setSelectedPosition] = useState<Position>("DEF");
|
||||
const [stats, setStats] = useState<GlobalStats>(() => createInitialStats());
|
||||
const [draft, setDraft] = useState<Record<Position, string | null>>(() => getFixedGuess(dailyPuzzles[0]));
|
||||
@@ -65,6 +66,9 @@ export default function Home({ sponsor }: HomeProps) {
|
||||
const activeProgress = progress[activeDifficulty];
|
||||
const isLocked = activeProgress.locked;
|
||||
const isFinished = activeProgress.solved || activeProgress.attempts.length >= MAX_ATTEMPTS;
|
||||
const hasFailed = !activeProgress.solved && activeProgress.attempts.length >= MAX_ATTEMPTS;
|
||||
const solutionRevealed = revealedSolutions[activeDifficulty] === true;
|
||||
const pitchDraft = solutionRevealed ? activePuzzle.solution : draft;
|
||||
const t = useMemo(() => getDictionary(locale), [locale]);
|
||||
const quote = useMemo(() => quoteForDay(dateKey, locale), [dateKey, locale]);
|
||||
const globalStatsShareText = useMemo(() => buildGlobalStatsShareText(stats, t), [stats, t]);
|
||||
@@ -211,6 +215,18 @@ export default function Home({ sponsor }: HomeProps) {
|
||||
setMessage(t.hintRevealed(positionToReveal));
|
||||
}
|
||||
|
||||
function revealSolution() {
|
||||
if (!hasFailed) return;
|
||||
|
||||
setRevealedSolutions((current) => ({ ...current, [activeDifficulty]: true }));
|
||||
setDraft(activePuzzle.solution);
|
||||
setMessage(t.solutionRevealed);
|
||||
trackEvent("solution_revealed", {
|
||||
difficulty: activeDifficulty,
|
||||
puzzle_number: getPuzzleNumber(dateKey),
|
||||
});
|
||||
}
|
||||
|
||||
function changeLocale(nextLocale: Locale) {
|
||||
setLocale(nextLocale);
|
||||
window.localStorage.setItem("hidden11-locale", nextLocale);
|
||||
@@ -239,6 +255,15 @@ export default function Home({ sponsor }: HomeProps) {
|
||||
|
||||
const actionButtons = (
|
||||
<div className="flex items-center gap-1.5">
|
||||
{hasFailed && !solutionRevealed ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={revealSolution}
|
||||
className="h-8 rounded-full border border-emerald-300/40 bg-emerald-300/15 px-3 text-[10px] font-black uppercase text-emerald-100 transition hover:bg-emerald-300/25"
|
||||
>
|
||||
{t.showSolution}
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
onClick={revealHint}
|
||||
@@ -398,11 +423,12 @@ export default function Home({ sponsor }: HomeProps) {
|
||||
<Pitch
|
||||
actions={actionButtons}
|
||||
collapsed={fieldCollapsed}
|
||||
draft={draft}
|
||||
draft={pitchDraft}
|
||||
fixedPosition={activePuzzle.fixedPosition}
|
||||
lastGuess={activeProgress.attempts[activeProgress.attempts.length - 1]}
|
||||
onToggleCollapsed={() => setFieldCollapsed((current) => !current)}
|
||||
selectedPosition={selectedPosition}
|
||||
solutionRevealed={solutionRevealed}
|
||||
t={t}
|
||||
onSelectPosition={setSelectedPosition}
|
||||
/>
|
||||
|
||||
@@ -12,6 +12,7 @@ type PitchProps = {
|
||||
lastGuess?: Guess;
|
||||
onToggleCollapsed: () => void;
|
||||
selectedPosition: Position;
|
||||
solutionRevealed?: boolean;
|
||||
t: Dictionary;
|
||||
onSelectPosition: (position: Position) => void;
|
||||
};
|
||||
@@ -32,10 +33,12 @@ export default function Pitch({
|
||||
lastGuess,
|
||||
onToggleCollapsed,
|
||||
selectedPosition,
|
||||
solutionRevealed = false,
|
||||
t,
|
||||
onSelectPosition,
|
||||
}: PitchProps) {
|
||||
function feedbackFor(position: Position): Feedback {
|
||||
if (solutionRevealed) return "correct";
|
||||
if (position === fixedPosition) return "correct";
|
||||
return lastGuess?.slots.find((slot) => slot.position === position)?.feedback ?? "empty";
|
||||
}
|
||||
|
||||
+7138
-3022
File diff suppressed because it is too large
Load Diff
+40
@@ -58,6 +58,7 @@ export type Dictionary = {
|
||||
puzzleFinished: string;
|
||||
searchPlaceholder: (position: string) => string;
|
||||
select: string;
|
||||
showSolution: string;
|
||||
share: string;
|
||||
challengeFriend: string;
|
||||
challengeShareLead: string;
|
||||
@@ -79,6 +80,7 @@ export type Dictionary = {
|
||||
bestTime: string;
|
||||
distribution: string;
|
||||
solvePrevious: string;
|
||||
solutionRevealed: string;
|
||||
solved: string;
|
||||
time: string;
|
||||
tries: string;
|
||||
@@ -145,6 +147,7 @@ const dictionaries: Record<Locale, Dictionary> = {
|
||||
puzzleFinished: "Puzzle finished.",
|
||||
searchPlaceholder: (position) => `Search player for ${position}`,
|
||||
select: "Select",
|
||||
showSolution: "Show solution",
|
||||
share: "Share",
|
||||
challengeFriend: "Challenge a friend",
|
||||
challengeShareLead: "I played today's hidden11. Can you beat me?",
|
||||
@@ -166,6 +169,7 @@ const dictionaries: Record<Locale, Dictionary> = {
|
||||
bestTime: "Best time",
|
||||
distribution: "Distribution",
|
||||
solvePrevious: "Solve the previous puzzle to unlock this lineup.",
|
||||
solutionRevealed: "Solution revealed.",
|
||||
solved: "solved",
|
||||
time: "Time",
|
||||
tries: "tries",
|
||||
@@ -230,6 +234,7 @@ const dictionaries: Record<Locale, Dictionary> = {
|
||||
puzzleFinished: "Puzzle terminado.",
|
||||
searchPlaceholder: (position) => `Buscar jugador para ${position}`,
|
||||
select: "Elegir",
|
||||
showSolution: "Ver solución",
|
||||
share: "Compartir",
|
||||
challengeFriend: "Retar a un amigo",
|
||||
challengeShareLead: "He jugado al hidden11 de hoy. ¿Puedes superarme?",
|
||||
@@ -251,6 +256,7 @@ const dictionaries: Record<Locale, Dictionary> = {
|
||||
bestTime: "Mejor tiempo",
|
||||
distribution: "Distribución",
|
||||
solvePrevious: "Resuelve el puzzle anterior para desbloquear esta alineación.",
|
||||
solutionRevealed: "Solución revelada.",
|
||||
solved: "resuelto",
|
||||
time: "Tiempo",
|
||||
tries: "intentos",
|
||||
@@ -329,6 +335,7 @@ const dictionaries: Record<Locale, Dictionary> = {
|
||||
puzzleFinished: "Puzzle terminé.",
|
||||
searchPlaceholder: (position) => `Chercher un joueur pour ${position}`,
|
||||
select: "Choisir",
|
||||
showSolution: "Voir la solution",
|
||||
share: "Partager",
|
||||
challengeFriend: "Défier un ami",
|
||||
challengeShareLead: "J'ai joué au hidden11 du jour. Peux-tu faire mieux ?",
|
||||
@@ -350,6 +357,7 @@ const dictionaries: Record<Locale, Dictionary> = {
|
||||
bestTime: "Meilleur temps",
|
||||
distribution: "Distribution",
|
||||
solvePrevious: "Résous le puzzle précédent pour déverrouiller cette équipe.",
|
||||
solutionRevealed: "Solution révélée.",
|
||||
solved: "résolu",
|
||||
time: "Temps",
|
||||
tries: "essais",
|
||||
@@ -414,6 +422,7 @@ const dictionaries: Record<Locale, Dictionary> = {
|
||||
puzzleFinished: "Rätsel beendet.",
|
||||
searchPlaceholder: (position) => `Spieler für ${position} suchen`,
|
||||
select: "Wählen",
|
||||
showSolution: "Lösung anzeigen",
|
||||
share: "Teilen",
|
||||
challengeFriend: "Freund herausfordern",
|
||||
challengeShareLead: "Ich habe das heutige hidden11 gespielt. Schaffst du mehr?",
|
||||
@@ -435,6 +444,7 @@ const dictionaries: Record<Locale, Dictionary> = {
|
||||
bestTime: "Beste Zeit",
|
||||
distribution: "Verteilung",
|
||||
solvePrevious: "Löse das vorherige Rätsel, um diese Aufstellung freizuschalten.",
|
||||
solutionRevealed: "Lösung angezeigt.",
|
||||
solved: "gelöst",
|
||||
time: "Zeit",
|
||||
tries: "Versuche",
|
||||
@@ -499,6 +509,7 @@ const dictionaries: Record<Locale, Dictionary> = {
|
||||
puzzleFinished: "Puzzle finito.",
|
||||
searchPlaceholder: (position) => `Cerca giocatore per ${position}`,
|
||||
select: "Scegli",
|
||||
showSolution: "Mostra soluzione",
|
||||
share: "Condividi",
|
||||
challengeFriend: "Sfida un amico",
|
||||
challengeShareLead: "Ho giocato all'hidden11 di oggi. Riesci a battermi?",
|
||||
@@ -520,6 +531,7 @@ const dictionaries: Record<Locale, Dictionary> = {
|
||||
bestTime: "Miglior tempo",
|
||||
distribution: "Distribuzione",
|
||||
solvePrevious: "Risolvi il puzzle precedente per sbloccare questa formazione.",
|
||||
solutionRevealed: "Soluzione rivelata.",
|
||||
solved: "risolto",
|
||||
time: "Tempo",
|
||||
tries: "tentativi",
|
||||
@@ -584,6 +596,7 @@ const dictionaries: Record<Locale, Dictionary> = {
|
||||
puzzleFinished: "Puzzle terminado.",
|
||||
searchPlaceholder: (position) => `Pesquisar jogador para ${position}`,
|
||||
select: "Escolher",
|
||||
showSolution: "Ver solução",
|
||||
share: "Partilhar",
|
||||
challengeFriend: "Desafiar um amigo",
|
||||
challengeShareLead: "Joguei o hidden11 de hoje. Consegues superar-me?",
|
||||
@@ -605,6 +618,7 @@ const dictionaries: Record<Locale, Dictionary> = {
|
||||
bestTime: "Melhor tempo",
|
||||
distribution: "Distribuição",
|
||||
solvePrevious: "Resolve o puzzle anterior para desbloquear este onze.",
|
||||
solutionRevealed: "Solução revelada.",
|
||||
solved: "resolvido",
|
||||
time: "Tempo",
|
||||
tries: "tentativas",
|
||||
@@ -671,6 +685,11 @@ function translateGeneratedClue(clue: string, locale: Locale): string {
|
||||
return tLeagueCount(locale, Number(leagueCount[1]), leagueCount[2]);
|
||||
}
|
||||
|
||||
const positionLeague = clue.match(/^The (.+) plays in (.+)\.$/);
|
||||
if (positionLeague) {
|
||||
return tPositionLeague(locale, positionLeague[1], positionLeague[2]);
|
||||
}
|
||||
|
||||
const differentClubs = clue.match(/^All five players come from different clubs\.$/);
|
||||
if (differentClubs) {
|
||||
return tAllDifferentClubs(locale);
|
||||
@@ -691,6 +710,11 @@ function translateGeneratedClue(clue: string, locale: Locale): string {
|
||||
return tWonLeagueTitles(locale, wonLeagueTitles[1], Number(wonLeagueTitles[2]));
|
||||
}
|
||||
|
||||
const wonChampionsTitles = clue.match(/^The (.+) has won (\d+) Champions League titles?\.$/);
|
||||
if (wonChampionsTitles) {
|
||||
return tWonChampionsTitles(locale, wonChampionsTitles[1], Number(wonChampionsTitles[2]));
|
||||
}
|
||||
|
||||
const wonCountries = clue.match(/^The (.+) has won league titles in (.+)\.$/);
|
||||
if (wonCountries) {
|
||||
return tWonCountries(locale, wonCountries[1], wonCountries[2]);
|
||||
@@ -878,6 +902,14 @@ function tLeagueCount(locale: Locale, count: number, league: string): string {
|
||||
return `${count} ${count === 1 ? "jogador joga" : "jogadores jogam"} na ${league}.`;
|
||||
}
|
||||
|
||||
function tPositionLeague(locale: Locale, position: string, league: string): string {
|
||||
if (locale === "es") return `El ${pos(locale, position)} juega en ${league}.`;
|
||||
if (locale === "fr") return `Le ${pos(locale, position)} évolue en ${league}.`;
|
||||
if (locale === "de") return `Der ${pos(locale, position)} spielt in der ${league}.`;
|
||||
if (locale === "it") return `Il ${pos(locale, position)} gioca in ${league}.`;
|
||||
return `O ${pos(locale, position)} joga na ${league}.`;
|
||||
}
|
||||
|
||||
function tAllDifferentClubs(locale: Locale): string {
|
||||
if (locale === "es") return "Los cinco jugadores son de clubes diferentes.";
|
||||
if (locale === "fr") return "Les cinq joueurs viennent de clubs différents.";
|
||||
@@ -910,6 +942,14 @@ function tWonLeagueTitles(locale: Locale, position: string, count: number): stri
|
||||
return `O ${pos(locale, position)} ganhou ${count} ${count === 1 ? "campeonato" : "campeonatos"}.`;
|
||||
}
|
||||
|
||||
function tWonChampionsTitles(locale: Locale, position: string, count: number): string {
|
||||
if (locale === "es") return `El ${pos(locale, position)} ha ganado ${count} ${count === 1 ? "Champions League" : "Champions League"}.`;
|
||||
if (locale === "fr") return `Le ${pos(locale, position)} a gagné ${count} ${count === 1 ? "Ligue des champions" : "Ligues des champions"}.`;
|
||||
if (locale === "de") return `Der ${pos(locale, position)} hat ${count} Champions-League-Titel gewonnen.`;
|
||||
if (locale === "it") return `Il ${pos(locale, position)} ha vinto ${count} ${count === 1 ? "Champions League" : "Champions League"}.`;
|
||||
return `O ${pos(locale, position)} ganhou ${count} ${count === 1 ? "Champions League" : "Champions League"}.`;
|
||||
}
|
||||
|
||||
function tWonCountries(locale: Locale, position: string, value: string): string {
|
||||
if (locale === "es") return `El ${pos(locale, position)} ha ganado ligas en ${countries(locale, value)}.`;
|
||||
if (locale === "fr") return `Le ${pos(locale, position)} a gagné des championnats en ${countries(locale, value)}.`;
|
||||
|
||||
@@ -3,6 +3,13 @@ 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));
|
||||
@@ -18,13 +25,14 @@ for (let day = 0; day < DAYS; day += 1) {
|
||||
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: POSITIONS[(ordinal + 2) % POSITIONS.length],
|
||||
clues: buildClues(lineup, difficulty),
|
||||
fixedPosition,
|
||||
clues: buildClues(lineup, difficulty, fixedPosition),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -63,46 +71,40 @@ function lineupFromIndex(index) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildClues(lineup, difficulty) {
|
||||
function buildClues(lineup, difficulty, fixedPosition) {
|
||||
const hiddenPositions = POSITIONS.filter((position) => position !== fixedPosition);
|
||||
|
||||
if (difficulty === "easy") {
|
||||
return [
|
||||
buildClubClue(lineup),
|
||||
buildLeagueCountClue(lineup),
|
||||
`The striker is from ${lineup.ST.nationality}.`,
|
||||
`The winger is from ${lineup.WING.nationality}.`,
|
||||
];
|
||||
return hiddenPositions.flatMap((position) => [
|
||||
buildNationalityClue(position, lineup[position]),
|
||||
buildCurrentLeagueClue(position, lineup[position]),
|
||||
]);
|
||||
}
|
||||
|
||||
if (difficulty === "medium") {
|
||||
return [
|
||||
buildTitleCountryClue(lineup.GK, "The goalkeeper"),
|
||||
buildTitleCountryClue(lineup.DEF, "The defender"),
|
||||
buildFormerClubClue(lineup.WING, lineup.ST, "The winger and striker"),
|
||||
buildNumericLeagueClue(lineup.MID, "The midfielder"),
|
||||
];
|
||||
return dedupeClues([
|
||||
...hiddenPositions.map((position) => buildTitleCountryClue(lineup[position], subjectFor(position))),
|
||||
...buildHiddenPairClues(lineup, hiddenPositions).slice(0, 2),
|
||||
]);
|
||||
}
|
||||
|
||||
return [
|
||||
buildTitleCountryPairClue(lineup.DEF, "The defender"),
|
||||
buildTitleCountryPairClue(lineup.MID, "The midfielder"),
|
||||
buildFormerClubClue(lineup.GK, lineup.MID, "The goalkeeper and midfielder"),
|
||||
buildLeagueTitleComparisonClue(lineup.GK, lineup.WING),
|
||||
buildChampionsLeagueComparisonClue(lineup.ST, lineup.DEF),
|
||||
];
|
||||
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 buildClubClue(lineup) {
|
||||
const clubCounts = countBy(Object.values(lineup), (player) => player.club);
|
||||
const shared = Object.entries(clubCounts).find(([, count]) => count > 1);
|
||||
if (!shared) return "All five players come from different clubs.";
|
||||
if (shared[1] === 2) return "Two players share the same club.";
|
||||
return `${shared[1]} players share the same club.`;
|
||||
function subjectFor(position) {
|
||||
return `The ${positionLabels[position]}`;
|
||||
}
|
||||
|
||||
function buildLeagueCountClue(lineup) {
|
||||
const leagueCounts = countBy(Object.values(lineup), (player) => player.league);
|
||||
const [league, count] = Object.entries(leagueCounts).sort((a, b) => b[1] - a[1])[0];
|
||||
return `${count} player${count === 1 ? "" : "s"} play in ${league}.`;
|
||||
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) {
|
||||
@@ -113,6 +115,10 @@ function buildTitleCountryClue(player, subject) {
|
||||
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);
|
||||
}
|
||||
@@ -135,6 +141,20 @@ function buildFormerClubClue(playerA, playerB, subject) {
|
||||
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) {
|
||||
@@ -150,32 +170,32 @@ function buildLeagueTitleCountClue(player, subject) {
|
||||
return `${subject} has won ${player.leagueTitles} league title${player.leagueTitles === 1 ? "" : "s"}.`;
|
||||
}
|
||||
|
||||
function buildLeagueTitleComparisonClue(playerA, playerB) {
|
||||
function buildLeagueTitleComparisonClue(positionA, playerA, positionB, playerB) {
|
||||
const subjectA = subjectFor(positionA);
|
||||
const subjectB = `the ${positionLabels[positionB]}`;
|
||||
if (playerA.leagueTitles > playerB.leagueTitles) {
|
||||
return `The goalkeeper has more league titles than the winger.`;
|
||||
return `${subjectA} has more league titles than ${subjectB}.`;
|
||||
}
|
||||
if (playerB.leagueTitles > playerA.leagueTitles) {
|
||||
return `The winger has more league titles than the goalkeeper.`;
|
||||
return `${subjectFor(positionB)} has more league titles than the ${positionLabels[positionA]}.`;
|
||||
}
|
||||
return `The goalkeeper and winger have won the same number of league titles.`;
|
||||
return `${subjectA} and ${positionLabels[positionB]} have won the same number of league titles.`;
|
||||
}
|
||||
|
||||
function buildChampionsLeagueComparisonClue(playerA, playerB) {
|
||||
function buildChampionsLeagueComparisonClue(positionA, playerA, positionB, playerB) {
|
||||
const subjectA = subjectFor(positionA);
|
||||
const subjectB = `the ${positionLabels[positionB]}`;
|
||||
if (playerA.championsLeagueTitles > playerB.championsLeagueTitles) {
|
||||
return `The striker has more Champions League titles than the defender.`;
|
||||
return `${subjectA} has more Champions League titles than ${subjectB}.`;
|
||||
}
|
||||
if (playerB.championsLeagueTitles > playerA.championsLeagueTitles) {
|
||||
return `The defender has more Champions League titles than the striker.`;
|
||||
return `${subjectFor(positionB)} has more Champions League titles than the ${positionLabels[positionA]}.`;
|
||||
}
|
||||
return `The striker and defender have won the same number of Champions League titles.`;
|
||||
return `${subjectA} and ${positionLabels[positionB]} have won the same number of Champions League titles.`;
|
||||
}
|
||||
|
||||
function countBy(items, getKey) {
|
||||
return items.reduce((acc, item) => {
|
||||
const key = getKey(item);
|
||||
acc[key] = (acc[key] ?? 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
function dedupeClues(clues) {
|
||||
return [...new Set(clues)];
|
||||
}
|
||||
|
||||
function addDays(dateKey, daysToAdd) {
|
||||
|
||||
@@ -3,6 +3,13 @@ import puzzles from "../data/puzzles.json" with { type: "json" };
|
||||
|
||||
const byId = new Map(players.map((player) => [player.id, player]));
|
||||
const positions = ["GK", "DEF", "MID", "WING", "ST"];
|
||||
const positionLabels = {
|
||||
GK: "goalkeeper",
|
||||
DEF: "defender",
|
||||
MID: "midfielder",
|
||||
WING: "winger",
|
||||
ST: "striker",
|
||||
};
|
||||
const dates = [...new Set(puzzles.map((puzzle) => puzzle.date))].sort();
|
||||
|
||||
function solutionPlayers(puzzle) {
|
||||
@@ -52,6 +59,29 @@ for (const puzzle of puzzles) {
|
||||
}
|
||||
}
|
||||
|
||||
const fixedLabel = positionLabels[puzzle.fixedPosition];
|
||||
if (puzzle.clues.some((clue) => clue.toLowerCase().includes(fixedLabel))) {
|
||||
throw new Error(`${puzzle.id}: clue references fixed position ${puzzle.fixedPosition}`);
|
||||
}
|
||||
|
||||
const mentionedHiddenPositions = new Set();
|
||||
for (const clue of puzzle.clues) {
|
||||
const normalized = clue.toLowerCase();
|
||||
for (const position of positions) {
|
||||
if (position === puzzle.fixedPosition) continue;
|
||||
if (normalized.includes(positionLabels[position])) {
|
||||
mentionedHiddenPositions.add(position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const position of positions) {
|
||||
if (position === puzzle.fixedPosition) continue;
|
||||
if (!mentionedHiddenPositions.has(position)) {
|
||||
throw new Error(`${puzzle.id}: no clue references hidden position ${position}`);
|
||||
}
|
||||
}
|
||||
|
||||
const lineup = solutionPlayers(puzzle);
|
||||
const signature = JSON.stringify(puzzle.solution);
|
||||
if (solutionSignatures.has(signature)) {
|
||||
|
||||
Reference in New Issue
Block a user