2a8d055da3
Build and push image / build (push) Successful in 1m19s
- 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>
123 lines
4.2 KiB
JavaScript
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";
|
|
}
|