Remove generic clues and document data sources
Build and push image / build (push) Successful in 1m17s

This commit is contained in:
alexandrev-tibco
2026-05-07 08:13:28 +02:00
parent 3048be5c29
commit 53214f96a5
6 changed files with 897 additions and 824 deletions
+2
View File
@@ -2,6 +2,8 @@
Daily football lineup puzzle built with Next.js, React, TypeScript and Tailwind CSS.
Player data provenance and clue rules are documented in `docs/data-sources.md`.
## Local development
```bash
+819 -819
View File
File diff suppressed because it is too large Load Diff
+50
View File
@@ -0,0 +1,50 @@
# Data Sources
Hidden11 uses a curated player dataset in `data/players.json`. The dataset is intentionally small enough to review manually, but each football fact should be checked against stable public sources before it is used in generated clues.
## Source Hierarchy
Use this order when adding or updating players:
1. Current club and registered squad:
Official league or club squad pages first. Examples include Premier League squad lists and official club squad pages.
2. Champions League titles:
UEFA's official Champions League records and honour boards.
3. Domestic league titles and career clubs:
Official league/club honour pages where available, then Wikidata/Wikipedia player pages for career history and honours.
4. Cross-check:
For Medium and Hard clues, compare at least two public sources when a title count or former-club relationship decides a clue.
## Public References
- UEFA Champions League all-time honours board:
`https://www.uefa.com/uefachampionsleague/news/0275-1541637ad1db-88aeeefefefd-1000--all-time-honours-board/`
- UEFA Champions League all-time rankings:
`https://www.uefa.com/uefachampionsleague/history/rankings/`
- Wikidata football data model:
`https://www.wikidata.org/wiki/Wikidata:WikiProject_Sports`
- Wikidata `member of sports team` property:
`https://www.wikidata.org/wiki/Property:P54`
## Clue Rules
- Easy clues may mention current leagues, clubs and nationalities.
- Medium and Hard clues should avoid giving away current team or nationality.
- Medium and Hard may use historical relationships such as shared former clubs, title counts, title countries and Champions League comparisons.
- Do not use generic clues that apply to almost every player in the dataset, for example "both have played in top-flight football".
## Maintenance
Run these commands after updating players or puzzles:
```bash
node scripts/generate-puzzles.mjs
npm run validate:data
npm run build
```
+1 -1
View File
@@ -686,7 +686,7 @@ function translateGeneratedClue(clue: string, locale: Locale): string {
return tSameChampions(locale, sameChampions[1], sameChampions[2]);
}
const wonLeagueTitles = clue.match(/^The (.+) has won (\d+) league titles\.$/);
const wonLeagueTitles = clue.match(/^The (.+) has won (\d+) league titles?\.$/);
if (wonLeagueTitles) {
return tWonLeagueTitles(locale, wonLeagueTitles[1], Number(wonLeagueTitles[2]));
}
+18 -4
View File
@@ -3,7 +3,7 @@ import players from "../data/players.json" with { type: "json" };
const POSITIONS = ["GK", "DEF", "MID", "WING", "ST"];
const DIFFICULTIES = ["easy", "medium", "hard"];
const START_DATE_KEY = getTodayKeyInTimeZone(new Date(), "Europe/Madrid");
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));
const totalCombinations = byPosition.reduce((product, group) => product * group.length, 1);
@@ -114,7 +114,7 @@ function buildTitleCountryClue(player, subject) {
}
function buildNumericLeagueClue(player, subject) {
return `${subject} has won ${player.leagueTitles} league titles.`;
return buildLeagueTitleCountClue(player, subject);
}
function buildFormerClubClue(playerA, playerB, subject) {
@@ -122,7 +122,17 @@ function buildFormerClubClue(playerA, playerB, subject) {
if (common) {
return `${subject} were once teammates at ${common}.`;
}
return `${subject} both have played in top-flight football.`;
if ((playerA.championsLeagueTitles ?? 0) === (playerB.championsLeagueTitles ?? 0)) {
return `${subject} have won the same number of Champions League titles.`;
}
if ((playerA.leagueTitles ?? 0) === (playerB.leagueTitles ?? 0)) {
return `${subject} have won the same number of league titles.`;
}
const firstCountry = playerA.leagueTitleCountries?.[0] ?? null;
if (firstCountry) {
return `${subject.split(" and ")[0]} has won league titles in ${firstCountry}.`;
}
return buildLeagueTitleCountClue(playerA, subject.split(" and ")[0]);
}
function buildTitleCountryPairClue(player, subject) {
@@ -133,7 +143,11 @@ function buildTitleCountryPairClue(player, subject) {
if (countries.length === 1) {
return `${subject} has won league titles in ${countries[0]}.`;
}
return `${subject} has won ${player.leagueTitles} league titles.`;
return buildLeagueTitleCountClue(player, subject);
}
function buildLeagueTitleCountClue(player, subject) {
return `${subject} has won ${player.leagueTitles} league title${player.leagueTitles === 1 ? "" : "s"}.`;
}
function buildLeagueTitleComparisonClue(playerA, playerB) {
+7
View File
@@ -35,6 +35,7 @@ for (let i = 1; i < dates.length; i += 1) {
}
const solutionSignatures = new Set();
const forbiddenClueFragments = ["top-flight football"];
for (const puzzle of puzzles) {
if (!positions.includes(puzzle.fixedPosition)) {
@@ -45,6 +46,12 @@ for (const puzzle of puzzles) {
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 lineup = solutionPlayers(puzzle);
const signature = JSON.stringify(puzzle.solution);
if (solutionSignatures.has(signature)) {