Remove generic clues and document data sources
Build and push image / build (push) Successful in 1m17s
Build and push image / build (push) Successful in 1m17s
This commit is contained in:
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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]));
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user