Files
hidden11/scripts/validate-puzzles.mjs
T
alexandrev-tibco f784c9c722
Build and push image / build (push) Successful in 1m19s
Reveal failed puzzles and improve clue coverage
2026-05-07 11:53:14 +02:00

100 lines
3.1 KiB
JavaScript

import players from "../data/players.json" with { type: "json" };
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) {
return Object.entries(puzzle.solution).map(([position, playerId]) => {
const player = byId.get(playerId);
if (!player) throw new Error(`${puzzle.id}: missing player ${playerId}`);
return { slot: position, ...player };
});
}
function toDateKey(date) {
return date.toISOString().slice(0, 10);
}
if (puzzles.length !== 365 * 3) {
throw new Error(`Expected ${365 * 3} puzzles, found ${puzzles.length}`);
}
if (dates.length !== 365) {
throw new Error(`Expected 365 unique dates, found ${dates.length}`);
}
for (let i = 1; i < dates.length; i += 1) {
const previous = new Date(`${dates[i - 1]}T00:00:00.000Z`);
previous.setUTCDate(previous.getUTCDate() + 1);
const expected = toDateKey(previous);
if (dates[i] !== expected) {
throw new Error(`Expected contiguous dates, got ${dates[i - 1]} then ${dates[i]}`);
}
}
const solutionSignatures = new Set();
const forbiddenClueFragments = ["top-flight football"];
for (const puzzle of puzzles) {
if (!positions.includes(puzzle.fixedPosition)) {
throw new Error(`${puzzle.id}: invalid fixed position ${puzzle.fixedPosition}`);
}
if (!Array.isArray(puzzle.clues) || puzzle.clues.length < 4) {
throw new Error(`${puzzle.id}: expected at least four clues`);
}
for (const clue of puzzle.clues) {
if (forbiddenClueFragments.some((fragment) => clue.includes(fragment))) {
throw new Error(`${puzzle.id}: forbidden clue "${clue}"`);
}
}
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)) {
throw new Error(`${puzzle.id}: duplicate solution detected`);
}
solutionSignatures.add(signature);
for (const player of lineup) {
if (!player.id || !player.name) {
throw new Error(`${puzzle.id}: invalid player reference`);
}
}
}
console.log("Puzzle data OK");