Files
hidden11/scripts/validate-puzzles.mjs
T
alexandrev-tibco 2a8d055da3
Build and push image / build (push) Successful in 1m19s
Fix Close label translation and replace liga with campeonato in clues
- Fix feedback color label: Close → Cerca/Proche/Nah/Vicino/Perto
  in all non-English locales (was using the verb "to close" instead
  of "near/close")
- Replace "liga/ligas" with "campeonato/campeonatos" in all ES
  translations that refer to league titles, to avoid confusion with
  the competition name LaLiga
- Add new player fields (ballonDOr, goldenBoot, worldCupWinner,
  copaAmericaWinner, eurocupWinner) and rework clue generation with
  difficulty-aware mixing (easy=all direct, medium=2 direct+2 harder,
  hard=1 direct+2 medium+comparisons)
- Add new clue types: club, trophy (World Cup/Euro/Copa), award
  (Ballon d'Or/Golden Boot), with full i18n support in 6 languages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 07:18:58 +02:00

123 lines
4.2 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`);
}
if (puzzle.clues.length > 6) {
throw new Error(`${puzzle.id}: expected no more than six clues`);
}
const clueTypes = new Set(puzzle.clues.map(classifyClue));
const minimumTypes = puzzle.difficulty === "easy" ? 2 : 3;
if (clueTypes.size < minimumTypes) {
throw new Error(`${puzzle.id}: expected at least ${minimumTypes} clue types, got ${[...clueTypes].join(", ")}`);
}
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");
function classifyClue(clue) {
if (clue.includes("teammates") || clue.includes("play for the same club")) return "history";
if (clue.includes("Champions League")) return "champions";
if (clue.includes("league title")) return "titles";
if (clue.includes("plays in") || clue.includes("top five leagues")) return "league";
if (clue.includes("plays for ")) return "club";
if (clue.includes("is from") || clue.includes("comes from")) return "nationality";
if (clue.includes("same club") || clue.includes("different clubs")) return "club";
if (clue.includes("World Cup") || clue.includes("European Championship") || clue.includes("Copa América")) return "trophy";
if (clue.includes("Ballon d'Or") || clue.includes("Golden Boot")) return "award";
return "other";
}