Fix Close label translation and replace liga with campeonato in clues
Build and push image / build (push) Successful in 1m19s
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>
This commit is contained in:
+235
-40
@@ -7,9 +7,14 @@
|
||||
"club": "Liverpool",
|
||||
"league": "Premier League",
|
||||
"formerClubs": ["Internacional", "Roma"],
|
||||
"championsLeagueTitles": 0,
|
||||
"championsLeagueTitles": 1,
|
||||
"leagueTitles": 2,
|
||||
"leagueTitleCountries": ["Brazil", "England"]
|
||||
"leagueTitleCountries": ["Brazil", "England"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "courtois",
|
||||
@@ -21,7 +26,12 @@
|
||||
"formerClubs": ["Genk", "Atletico Madrid", "Chelsea"],
|
||||
"championsLeagueTitles": 2,
|
||||
"leagueTitles": 4,
|
||||
"leagueTitleCountries": ["Spain", "England"]
|
||||
"leagueTitleCountries": ["Spain", "England"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "donnarumma",
|
||||
@@ -33,7 +43,12 @@
|
||||
"formerClubs": ["AC Milan", "Paris Saint-Germain"],
|
||||
"championsLeagueTitles": 0,
|
||||
"leagueTitles": 2,
|
||||
"leagueTitleCountries": ["France"]
|
||||
"leagueTitleCountries": ["France"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": true
|
||||
},
|
||||
{
|
||||
"id": "ederson",
|
||||
@@ -45,7 +60,12 @@
|
||||
"formerClubs": ["Rio Ave", "Benfica"],
|
||||
"championsLeagueTitles": 1,
|
||||
"leagueTitles": 7,
|
||||
"leagueTitleCountries": ["Portugal", "England"]
|
||||
"leagueTitleCountries": ["Portugal", "England"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "mamardashvili",
|
||||
@@ -57,7 +77,12 @@
|
||||
"formerClubs": ["Dinamo Tbilisi", "Valencia"],
|
||||
"championsLeagueTitles": 0,
|
||||
"leagueTitles": 0,
|
||||
"leagueTitleCountries": []
|
||||
"leagueTitleCountries": [],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "trubin",
|
||||
@@ -69,7 +94,12 @@
|
||||
"formerClubs": ["Shakhtar Donetsk"],
|
||||
"championsLeagueTitles": 0,
|
||||
"leagueTitles": 2,
|
||||
"leagueTitleCountries": ["Ukraine", "Portugal"]
|
||||
"leagueTitleCountries": ["Ukraine", "Portugal"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "van-dijk",
|
||||
@@ -81,7 +111,12 @@
|
||||
"formerClubs": ["Groningen", "Celtic", "Southampton"],
|
||||
"championsLeagueTitles": 1,
|
||||
"leagueTitles": 2,
|
||||
"leagueTitleCountries": ["Scotland", "England"]
|
||||
"leagueTitleCountries": ["Scotland", "England"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "dias",
|
||||
@@ -93,7 +128,12 @@
|
||||
"formerClubs": ["Benfica"],
|
||||
"championsLeagueTitles": 1,
|
||||
"leagueTitles": 5,
|
||||
"leagueTitleCountries": ["Portugal", "England"]
|
||||
"leagueTitleCountries": ["Portugal", "England"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "rudiger",
|
||||
@@ -105,7 +145,12 @@
|
||||
"formerClubs": ["Stuttgart", "Roma", "Chelsea"],
|
||||
"championsLeagueTitles": 2,
|
||||
"leagueTitles": 1,
|
||||
"leagueTitleCountries": ["Spain"]
|
||||
"leagueTitleCountries": ["Spain"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "saliba",
|
||||
@@ -117,7 +162,12 @@
|
||||
"formerClubs": ["Saint-Etienne", "Nice", "Marseille"],
|
||||
"championsLeagueTitles": 0,
|
||||
"leagueTitles": 0,
|
||||
"leagueTitleCountries": []
|
||||
"leagueTitleCountries": [],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "pavard",
|
||||
@@ -129,7 +179,12 @@
|
||||
"formerClubs": ["Lille", "Stuttgart", "Bayern Munich"],
|
||||
"championsLeagueTitles": 1,
|
||||
"leagueTitles": 4,
|
||||
"leagueTitleCountries": ["Germany", "Italy"]
|
||||
"leagueTitleCountries": ["Germany", "Italy"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": true,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "grimaldo",
|
||||
@@ -141,7 +196,12 @@
|
||||
"formerClubs": ["Barcelona B", "Benfica"],
|
||||
"championsLeagueTitles": 0,
|
||||
"leagueTitles": 5,
|
||||
"leagueTitleCountries": ["Portugal", "Germany"]
|
||||
"leagueTitleCountries": ["Portugal", "Germany"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": true
|
||||
},
|
||||
{
|
||||
"id": "bastoni",
|
||||
@@ -153,7 +213,12 @@
|
||||
"formerClubs": ["Atalanta", "Parma"],
|
||||
"championsLeagueTitles": 0,
|
||||
"leagueTitles": 2,
|
||||
"leagueTitleCountries": ["Italy"]
|
||||
"leagueTitleCountries": ["Italy"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": true
|
||||
},
|
||||
{
|
||||
"id": "calafiori",
|
||||
@@ -165,7 +230,12 @@
|
||||
"formerClubs": ["Roma", "Basel", "Bologna"],
|
||||
"championsLeagueTitles": 0,
|
||||
"leagueTitles": 1,
|
||||
"leagueTitleCountries": ["Switzerland"]
|
||||
"leagueTitleCountries": ["Switzerland"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "rodri",
|
||||
@@ -177,7 +247,12 @@
|
||||
"formerClubs": ["Villarreal", "Atletico Madrid"],
|
||||
"championsLeagueTitles": 1,
|
||||
"leagueTitles": 5,
|
||||
"leagueTitleCountries": ["England", "Spain"]
|
||||
"leagueTitleCountries": ["England", "Spain"],
|
||||
"ballonDOr": 1,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": true
|
||||
},
|
||||
{
|
||||
"id": "bellingham",
|
||||
@@ -189,7 +264,12 @@
|
||||
"formerClubs": ["Birmingham City", "Borussia Dortmund"],
|
||||
"championsLeagueTitles": 1,
|
||||
"leagueTitles": 1,
|
||||
"leagueTitleCountries": ["Spain"]
|
||||
"leagueTitleCountries": ["Spain"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "modric",
|
||||
@@ -201,7 +281,12 @@
|
||||
"formerClubs": ["Dinamo Zagreb", "Tottenham Hotspur", "Real Madrid"],
|
||||
"championsLeagueTitles": 6,
|
||||
"leagueTitles": 6,
|
||||
"leagueTitleCountries": ["Croatia", "Spain"]
|
||||
"leagueTitleCountries": ["Croatia", "Spain"],
|
||||
"ballonDOr": 1,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "odegaard",
|
||||
@@ -213,7 +298,12 @@
|
||||
"formerClubs": ["Stromsgodset", "Real Madrid", "Real Sociedad"],
|
||||
"championsLeagueTitles": 0,
|
||||
"leagueTitles": 1,
|
||||
"leagueTitleCountries": ["Netherlands"]
|
||||
"leagueTitleCountries": ["Netherlands"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "de-bruyne",
|
||||
@@ -225,7 +315,12 @@
|
||||
"formerClubs": ["Genk", "Chelsea", "Werder Bremen", "Wolfsburg", "Manchester City"],
|
||||
"championsLeagueTitles": 1,
|
||||
"leagueTitles": 7,
|
||||
"leagueTitleCountries": ["Belgium", "England"]
|
||||
"leagueTitleCountries": ["Belgium", "England"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "szoboszlai",
|
||||
@@ -237,7 +332,12 @@
|
||||
"formerClubs": ["FC Liefering", "Red Bull Salzburg", "RB Leipzig"],
|
||||
"championsLeagueTitles": 0,
|
||||
"leagueTitles": 6,
|
||||
"leagueTitleCountries": ["Austria", "Germany", "England"]
|
||||
"leagueTitleCountries": ["Austria", "Germany", "England"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "fabian-ruiz",
|
||||
@@ -249,7 +349,12 @@
|
||||
"formerClubs": ["Real Betis", "Napoli"],
|
||||
"championsLeagueTitles": 0,
|
||||
"leagueTitles": 2,
|
||||
"leagueTitleCountries": ["Italy", "France"]
|
||||
"leagueTitleCountries": ["Italy", "France"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": true
|
||||
},
|
||||
{
|
||||
"id": "vitinha",
|
||||
@@ -261,7 +366,12 @@
|
||||
"formerClubs": ["Porto", "Wolverhampton Wanderers"],
|
||||
"championsLeagueTitles": 0,
|
||||
"leagueTitles": 3,
|
||||
"leagueTitleCountries": ["Portugal", "France"]
|
||||
"leagueTitleCountries": ["Portugal", "France"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "joao-neves",
|
||||
@@ -273,7 +383,12 @@
|
||||
"formerClubs": ["Benfica"],
|
||||
"championsLeagueTitles": 0,
|
||||
"leagueTitles": 1,
|
||||
"leagueTitleCountries": ["France"]
|
||||
"leagueTitleCountries": ["France"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "saka",
|
||||
@@ -285,7 +400,12 @@
|
||||
"formerClubs": [],
|
||||
"championsLeagueTitles": 0,
|
||||
"leagueTitles": 0,
|
||||
"leagueTitleCountries": []
|
||||
"leagueTitleCountries": [],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "vinicius",
|
||||
@@ -297,7 +417,12 @@
|
||||
"formerClubs": ["Flamengo"],
|
||||
"championsLeagueTitles": 2,
|
||||
"leagueTitles": 3,
|
||||
"leagueTitleCountries": ["Spain"]
|
||||
"leagueTitleCountries": ["Spain"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": true,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "salah",
|
||||
@@ -309,7 +434,12 @@
|
||||
"formerClubs": ["Basel", "Chelsea", "Fiorentina", "Roma"],
|
||||
"championsLeagueTitles": 1,
|
||||
"leagueTitles": 3,
|
||||
"leagueTitleCountries": ["Switzerland", "England"]
|
||||
"leagueTitleCountries": ["Switzerland", "England"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 3,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "mbappe",
|
||||
@@ -321,7 +451,12 @@
|
||||
"formerClubs": ["Monaco", "Paris Saint-Germain"],
|
||||
"championsLeagueTitles": 1,
|
||||
"leagueTitles": 7,
|
||||
"leagueTitleCountries": ["France", "Spain"]
|
||||
"leagueTitleCountries": ["France", "Spain"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": true,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "coman",
|
||||
@@ -333,7 +468,12 @@
|
||||
"formerClubs": ["Paris Saint-Germain", "Juventus"],
|
||||
"championsLeagueTitles": 1,
|
||||
"leagueTitles": 12,
|
||||
"leagueTitleCountries": ["France", "Italy", "Germany"]
|
||||
"leagueTitleCountries": ["France", "Italy", "Germany"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": true,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "kvaratskhelia",
|
||||
@@ -345,7 +485,12 @@
|
||||
"formerClubs": ["Dinamo Batumi", "Rubin Kazan", "Napoli"],
|
||||
"championsLeagueTitles": 0,
|
||||
"leagueTitles": 2,
|
||||
"leagueTitleCountries": ["Italy", "France"]
|
||||
"leagueTitleCountries": ["Italy", "France"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "olise",
|
||||
@@ -357,7 +502,12 @@
|
||||
"formerClubs": ["Reading", "Crystal Palace"],
|
||||
"championsLeagueTitles": 0,
|
||||
"leagueTitles": 1,
|
||||
"leagueTitleCountries": ["Germany"]
|
||||
"leagueTitleCountries": ["Germany"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "barcola",
|
||||
@@ -369,7 +519,12 @@
|
||||
"formerClubs": ["Lyon"],
|
||||
"championsLeagueTitles": 0,
|
||||
"leagueTitles": 2,
|
||||
"leagueTitleCountries": ["France"]
|
||||
"leagueTitleCountries": ["France"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "haaland",
|
||||
@@ -381,7 +536,12 @@
|
||||
"formerClubs": ["Molde", "Red Bull Salzburg", "Borussia Dortmund"],
|
||||
"championsLeagueTitles": 1,
|
||||
"leagueTitles": 4,
|
||||
"leagueTitleCountries": ["Austria", "England"]
|
||||
"leagueTitleCountries": ["Austria", "England"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 1,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "kane",
|
||||
@@ -393,7 +553,12 @@
|
||||
"formerClubs": ["Tottenham Hotspur", "Leicester City", "Norwich City"],
|
||||
"championsLeagueTitles": 0,
|
||||
"leagueTitles": 1,
|
||||
"leagueTitleCountries": ["Germany"]
|
||||
"leagueTitleCountries": ["Germany"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "lewandowski",
|
||||
@@ -405,7 +570,12 @@
|
||||
"formerClubs": ["Lech Poznan", "Borussia Dortmund", "Bayern Munich"],
|
||||
"championsLeagueTitles": 1,
|
||||
"leagueTitles": 11,
|
||||
"leagueTitleCountries": ["Poland", "Germany", "Spain"]
|
||||
"leagueTitleCountries": ["Poland", "Germany", "Spain"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 2,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "lautaro",
|
||||
@@ -417,7 +587,12 @@
|
||||
"formerClubs": ["Racing Club"],
|
||||
"championsLeagueTitles": 0,
|
||||
"leagueTitles": 2,
|
||||
"leagueTitleCountries": ["Italy"]
|
||||
"leagueTitleCountries": ["Italy"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": true,
|
||||
"copaAmericaWinner": true,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "morata",
|
||||
@@ -429,7 +604,12 @@
|
||||
"formerClubs": ["Real Madrid", "Juventus", "Chelsea", "Atletico Madrid", "AC Milan"],
|
||||
"championsLeagueTitles": 2,
|
||||
"leagueTitles": 4,
|
||||
"leagueTitleCountries": ["Spain", "Italy"]
|
||||
"leagueTitleCountries": ["Spain", "Italy"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": true
|
||||
},
|
||||
{
|
||||
"id": "jonathan-david",
|
||||
@@ -441,7 +621,12 @@
|
||||
"formerClubs": ["Gent", "Lille"],
|
||||
"championsLeagueTitles": 0,
|
||||
"leagueTitles": 1,
|
||||
"leagueTitleCountries": ["France"]
|
||||
"leagueTitleCountries": ["France"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "gyokeres",
|
||||
@@ -453,7 +638,12 @@
|
||||
"formerClubs": ["Brommapojkarna", "Coventry City", "Sporting CP"],
|
||||
"championsLeagueTitles": 0,
|
||||
"leagueTitles": 1,
|
||||
"leagueTitleCountries": ["Portugal"]
|
||||
"leagueTitleCountries": ["Portugal"],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 1,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": false
|
||||
},
|
||||
{
|
||||
"id": "retegui",
|
||||
@@ -465,6 +655,11 @@
|
||||
"formerClubs": ["Tigre", "Genoa"],
|
||||
"championsLeagueTitles": 0,
|
||||
"leagueTitles": 0,
|
||||
"leagueTitleCountries": []
|
||||
"leagueTitleCountries": [],
|
||||
"ballonDOr": 0,
|
||||
"goldenBoot": 0,
|
||||
"worldCupWinner": false,
|
||||
"copaAmericaWinner": false,
|
||||
"eurocupWinner": false
|
||||
}
|
||||
]
|
||||
|
||||
+3845
-3845
File diff suppressed because it is too large
Load Diff
+122
-19
@@ -191,7 +191,7 @@ const dictionaries: Record<Locale, Dictionary> = {
|
||||
allCompleted: "Todos los puzzles completados.",
|
||||
available: "disponible",
|
||||
browsePlayers: (position) => `Ver todos los jugadores ${position}`,
|
||||
close: "Cerrar",
|
||||
close: "Cerca",
|
||||
closeModal: "Cerrar lista de jugadores",
|
||||
copied: "Copiado",
|
||||
copy: "Copiar",
|
||||
@@ -277,13 +277,13 @@ const dictionaries: Record<Locale, Dictionary> = {
|
||||
"Four players play in England.": "Cuatro jugadores juegan en Inglaterra.",
|
||||
"The striker is Norwegian.": "El delantero es noruego.",
|
||||
"The winger is English.": "El extremo es inglés.",
|
||||
"The goalkeeper and striker have both won league titles in Spain.": "El portero y el delantero han ganado ligas en España.",
|
||||
"The defender and winger have both won league titles in Germany.": "El defensa y el extremo han ganado ligas en Alemania.",
|
||||
"The goalkeeper and striker have both won league titles in Spain.": "El portero y el delantero han ganado campeonatos en España.",
|
||||
"The defender and winger have both won league titles in Germany.": "El defensa y el extremo han ganado campeonatos en Alemania.",
|
||||
"The winger and striker were once teammates at Juventus.": "El extremo y el delantero coincidieron en la Juventus.",
|
||||
"The midfielder has played league football in Austria, Germany and England.": "El centrocampista ha jugado liga en Austria, Alemania e Inglaterra.",
|
||||
"The goalkeeper and defender have both won league titles in Portugal.": "El portero y el defensa han ganado ligas en Portugal.",
|
||||
"The defender has won league titles in Portugal and Germany.": "El defensa ha ganado ligas en Portugal y Alemania.",
|
||||
"The midfielder has won league titles in Italy and France.": "El centrocampista ha ganado ligas en Italia y Francia.",
|
||||
"The midfielder has played league football in Austria, Germany and England.": "El centrocampista ha jugado en liga en Austria, Alemania e Inglaterra.",
|
||||
"The goalkeeper and defender have both won league titles in Portugal.": "El portero y el defensa han ganado campeonatos en Portugal.",
|
||||
"The defender has won league titles in Portugal and Germany.": "El defensa ha ganado campeonatos en Portugal y Alemania.",
|
||||
"The midfielder has won league titles in Italy and France.": "El centrocampista ha ganado campeonatos en Italia y Francia.",
|
||||
"The winger and midfielder both moved to France after starring in Italy.": "El extremo y el centrocampista llegaron a Francia después de destacar en Italia.",
|
||||
"The striker was the top scorer in France before moving to Italy.": "El delantero fue máximo goleador en Francia antes de irse a Italia.",
|
||||
},
|
||||
@@ -292,7 +292,7 @@ const dictionaries: Record<Locale, Dictionary> = {
|
||||
allCompleted: "Tous les puzzles sont terminés.",
|
||||
available: "disponible",
|
||||
browsePlayers: (position) => `Voir tous les joueurs ${position}`,
|
||||
close: "Fermer",
|
||||
close: "Proche",
|
||||
closeModal: "Fermer la liste des joueurs",
|
||||
copied: "Copié",
|
||||
copy: "Copier",
|
||||
@@ -379,7 +379,7 @@ const dictionaries: Record<Locale, Dictionary> = {
|
||||
allCompleted: "Alle Rätsel abgeschlossen.",
|
||||
available: "verfügbar",
|
||||
browsePlayers: (position) => `Alle ${position}-Spieler anzeigen`,
|
||||
close: "Schliessen",
|
||||
close: "Nah",
|
||||
closeModal: "Spielerliste schließen",
|
||||
copied: "Kopiert",
|
||||
copy: "Kopieren",
|
||||
@@ -466,7 +466,7 @@ const dictionaries: Record<Locale, Dictionary> = {
|
||||
allCompleted: "Tutti i puzzle completati.",
|
||||
available: "disponibile",
|
||||
browsePlayers: (position) => `Vedi tutti i giocatori ${position}`,
|
||||
close: "Chiudi",
|
||||
close: "Vicino",
|
||||
closeModal: "Chiudi lista giocatori",
|
||||
copied: "Copiato",
|
||||
copy: "Copia",
|
||||
@@ -553,7 +553,7 @@ const dictionaries: Record<Locale, Dictionary> = {
|
||||
allCompleted: "Todos os puzzles concluídos.",
|
||||
available: "disponível",
|
||||
browsePlayers: (position) => `Ver todos os jogadores ${position}`,
|
||||
close: "Fechar",
|
||||
close: "Perto",
|
||||
closeModal: "Fechar lista de jogadores",
|
||||
copied: "Copiado",
|
||||
copy: "Copiar",
|
||||
@@ -674,6 +674,46 @@ function translateGeneratedClue(clue: string, locale: Locale): string {
|
||||
return clue;
|
||||
}
|
||||
|
||||
const playsFor = clue.match(/^The (.+) plays for (.+)\.$/);
|
||||
if (playsFor) {
|
||||
return tPlaysFor(locale, playsFor[1], playsFor[2]);
|
||||
}
|
||||
|
||||
const wonWorldCup = clue.match(/^The (.+) has won the World Cup\.$/);
|
||||
if (wonWorldCup) {
|
||||
return tWonWorldCup(locale, wonWorldCup[1]);
|
||||
}
|
||||
|
||||
const wonEuro = clue.match(/^The (.+) has won the European Championship\.$/);
|
||||
if (wonEuro) {
|
||||
return tWonEuro(locale, wonEuro[1]);
|
||||
}
|
||||
|
||||
const wonCopa = clue.match(/^The (.+) has won the Copa América\.$/);
|
||||
if (wonCopa) {
|
||||
return tWonCopaAmerica(locale, wonCopa[1]);
|
||||
}
|
||||
|
||||
const wonBallonDOr = clue.match(/^The (.+) has won the Ballon d'Or\.$/);
|
||||
if (wonBallonDOr) {
|
||||
return tWonBallonDOr(locale, wonBallonDOr[1]);
|
||||
}
|
||||
|
||||
const wonGoldenBootMultiple = clue.match(/^The (.+) has won (\d+) European Golden Boots\.$/);
|
||||
if (wonGoldenBootMultiple) {
|
||||
return tWonGoldenBoots(locale, wonGoldenBootMultiple[1], Number(wonGoldenBootMultiple[2]));
|
||||
}
|
||||
|
||||
const wonGoldenBoot = clue.match(/^The (.+) has won the European Golden Boot\.$/);
|
||||
if (wonGoldenBoot) {
|
||||
return tWonGoldenBoots(locale, wonGoldenBoot[1], 1);
|
||||
}
|
||||
|
||||
const playForSameClub = clue.match(/^The (.+) and (.+) play for the same club\.$/);
|
||||
if (playForSameClub) {
|
||||
return tPlayForSameClub(locale, playForSameClub[1], playForSameClub[2]);
|
||||
}
|
||||
|
||||
const sharedClub = clue.match(/^(\d+|Two) players share the same club\.$/);
|
||||
if (sharedClub) {
|
||||
const count = sharedClub[1] === "Two" ? 2 : Number(sharedClub[1]);
|
||||
@@ -994,7 +1034,7 @@ function tSameChampions(locale: Locale, left: string, right: string): string {
|
||||
}
|
||||
|
||||
function tWonLeagueTitles(locale: Locale, position: string, count: number): string {
|
||||
if (locale === "es") return `El ${pos(locale, position)} ha ganado ${count} ${count === 1 ? "liga" : "ligas"}.`;
|
||||
if (locale === "es") return `El ${pos(locale, position)} ha ganado ${count} ${count === 1 ? "campeonato" : "campeonatos"}.`;
|
||||
if (locale === "fr") return `Le ${pos(locale, position)} a gagné ${count} ${count === 1 ? "championnat" : "championnats"}.`;
|
||||
if (locale === "de") return `Der ${pos(locale, position)} hat ${count} Ligatitel gewonnen.`;
|
||||
if (locale === "it") return `Il ${pos(locale, position)} ha vinto ${count} ${count === 1 ? "campionato" : "campionati"}.`;
|
||||
@@ -1002,7 +1042,7 @@ function tWonLeagueTitles(locale: Locale, position: string, count: number): stri
|
||||
}
|
||||
|
||||
function tNoLeagueTitle(locale: Locale, position: string): string {
|
||||
if (locale === "es") return `El ${pos(locale, position)} no ha ganado ninguna liga.`;
|
||||
if (locale === "es") return `El ${pos(locale, position)} no ha ganado ningún campeonato.`;
|
||||
if (locale === "fr") return `Le ${pos(locale, position)} n'a gagné aucun championnat.`;
|
||||
if (locale === "de") return `Der ${pos(locale, position)} hat keinen Ligatitel gewonnen.`;
|
||||
if (locale === "it") return `Il ${pos(locale, position)} non ha vinto alcun campionato.`;
|
||||
@@ -1010,7 +1050,7 @@ function tNoLeagueTitle(locale: Locale, position: string): string {
|
||||
}
|
||||
|
||||
function tAtMostLeagueTitles(locale: Locale, position: string, count: number): string {
|
||||
if (locale === "es") return `El ${pos(locale, position)} ha ganado como máximo ${count} ligas.`;
|
||||
if (locale === "es") return `El ${pos(locale, position)} ha ganado como máximo ${count} campeonatos.`;
|
||||
if (locale === "fr") return `Le ${pos(locale, position)} a gagné au maximum ${count} championnats.`;
|
||||
if (locale === "de") return `Der ${pos(locale, position)} hat höchstens ${count} Ligatitel gewonnen.`;
|
||||
if (locale === "it") return `Il ${pos(locale, position)} ha vinto al massimo ${count} campionati.`;
|
||||
@@ -1018,7 +1058,7 @@ function tAtMostLeagueTitles(locale: Locale, position: string, count: number): s
|
||||
}
|
||||
|
||||
function tBetweenLeagueTitles(locale: Locale, position: string, min: number, max: number): string {
|
||||
if (locale === "es") return `El ${pos(locale, position)} ha ganado entre ${min} y ${max} ligas.`;
|
||||
if (locale === "es") return `El ${pos(locale, position)} ha ganado entre ${min} y ${max} campeonatos.`;
|
||||
if (locale === "fr") return `Le ${pos(locale, position)} a gagné entre ${min} et ${max} championnats.`;
|
||||
if (locale === "de") return `Der ${pos(locale, position)} hat zwischen ${min} und ${max} Ligatitel gewonnen.`;
|
||||
if (locale === "it") return `Il ${pos(locale, position)} ha vinto tra ${min} e ${max} campionati.`;
|
||||
@@ -1026,7 +1066,7 @@ function tBetweenLeagueTitles(locale: Locale, position: string, min: number, max
|
||||
}
|
||||
|
||||
function tMoreThanLeagueTitles(locale: Locale, position: string, count: number): string {
|
||||
if (locale === "es") return `El ${pos(locale, position)} ha ganado más de ${count} ligas.`;
|
||||
if (locale === "es") return `El ${pos(locale, position)} ha ganado más de ${count} campeonatos.`;
|
||||
if (locale === "fr") return `Le ${pos(locale, position)} a gagné plus de ${count} championnats.`;
|
||||
if (locale === "de") return `Der ${pos(locale, position)} hat mehr als ${count} Ligatitel gewonnen.`;
|
||||
if (locale === "it") return `Il ${pos(locale, position)} ha vinto più di ${count} campionati.`;
|
||||
@@ -1042,7 +1082,7 @@ function tWonChampionsTitles(locale: Locale, position: string, count: number): s
|
||||
}
|
||||
|
||||
function tWonCountries(locale: Locale, position: string, value: string): string {
|
||||
if (locale === "es") return `El ${pos(locale, position)} ha ganado ligas en ${countries(locale, value)}.`;
|
||||
if (locale === "es") return `El ${pos(locale, position)} ha ganado campeonatos en ${countries(locale, value)}.`;
|
||||
if (locale === "fr") return `Le ${pos(locale, position)} a gagné des championnats en ${countries(locale, value)}.`;
|
||||
if (locale === "de") return `Der ${pos(locale, position)} hat Ligatitel in ${countries(locale, value)} gewonnen.`;
|
||||
if (locale === "it") return `Il ${pos(locale, position)} ha vinto campionati in ${countries(locale, value)}.`;
|
||||
@@ -1066,7 +1106,7 @@ function tTeammates(locale: Locale, left: string, right: string, club: string):
|
||||
}
|
||||
|
||||
function tSameLeagueTitles(locale: Locale, left: string, right: string): string {
|
||||
if (locale === "es") return `El ${pos(locale, left)} y el ${pos(locale, right)} han ganado el mismo número de ligas.`;
|
||||
if (locale === "es") return `El ${pos(locale, left)} y el ${pos(locale, right)} han ganado el mismo número de campeonatos.`;
|
||||
if (locale === "fr") return `Le ${pos(locale, left)} et le ${pos(locale, right)} ont gagné le même nombre de championnats.`;
|
||||
if (locale === "de") return `Der ${pos(locale, left)} und der ${pos(locale, right)} haben gleich viele Ligatitel gewonnen.`;
|
||||
if (locale === "it") return `Il ${pos(locale, left)} e il ${pos(locale, right)} hanno vinto lo stesso numero di campionati.`;
|
||||
@@ -1074,7 +1114,7 @@ function tSameLeagueTitles(locale: Locale, left: string, right: string): string
|
||||
}
|
||||
|
||||
function tMoreLeagueTitles(locale: Locale, left: string, right: string): string {
|
||||
if (locale === "es") return `El ${pos(locale, left)} tiene más ligas que el ${pos(locale, right)}.`;
|
||||
if (locale === "es") return `El ${pos(locale, left)} tiene más campeonatos que el ${pos(locale, right)}.`;
|
||||
if (locale === "fr") return `Le ${pos(locale, left)} a plus de championnats que le ${pos(locale, right)}.`;
|
||||
if (locale === "de") return `Der ${pos(locale, left)} hat mehr Ligatitel als der ${pos(locale, right)}.`;
|
||||
if (locale === "it") return `Il ${pos(locale, left)} ha più campionati del ${pos(locale, right)}.`;
|
||||
@@ -1088,3 +1128,66 @@ function tNationality(locale: Locale, position: string, country: string): string
|
||||
if (locale === "it") return `Il ${pos(locale, position)} viene da ${countries(locale, country)}.`;
|
||||
return `O ${pos(locale, position)} é de ${countries(locale, country)}.`;
|
||||
}
|
||||
|
||||
function tPlaysFor(locale: Locale, position: string, club: string): string {
|
||||
if (locale === "es") return `El ${pos(locale, position)} juega en el ${club}.`;
|
||||
if (locale === "fr") return `Le ${pos(locale, position)} joue au ${club}.`;
|
||||
if (locale === "de") return `Der ${pos(locale, position)} spielt für den ${club}.`;
|
||||
if (locale === "it") return `Il ${pos(locale, position)} gioca nell'${club}.`;
|
||||
return `O ${pos(locale, position)} joga no ${club}.`;
|
||||
}
|
||||
|
||||
function tWonWorldCup(locale: Locale, position: string): string {
|
||||
if (locale === "es") return `El ${pos(locale, position)} es campeón del mundo.`;
|
||||
if (locale === "fr") return `Le ${pos(locale, position)} est champion du monde.`;
|
||||
if (locale === "de") return `Der ${pos(locale, position)} ist Weltmeister.`;
|
||||
if (locale === "it") return `Il ${pos(locale, position)} è campione del mondo.`;
|
||||
return `O ${pos(locale, position)} é campeão do mundo.`;
|
||||
}
|
||||
|
||||
function tWonEuro(locale: Locale, position: string): string {
|
||||
if (locale === "es") return `El ${pos(locale, position)} ha ganado la Eurocopa.`;
|
||||
if (locale === "fr") return `Le ${pos(locale, position)} a remporté le Championnat d'Europe.`;
|
||||
if (locale === "de") return `Der ${pos(locale, position)} hat die Europameisterschaft gewonnen.`;
|
||||
if (locale === "it") return `Il ${pos(locale, position)} ha vinto il Campionato Europeo.`;
|
||||
return `O ${pos(locale, position)} ganhou o Campeonato da Europa.`;
|
||||
}
|
||||
|
||||
function tWonCopaAmerica(locale: Locale, position: string): string {
|
||||
if (locale === "es") return `El ${pos(locale, position)} ha ganado la Copa América.`;
|
||||
if (locale === "fr") return `Le ${pos(locale, position)} a remporté la Copa América.`;
|
||||
if (locale === "de") return `Der ${pos(locale, position)} hat die Copa América gewonnen.`;
|
||||
if (locale === "it") return `Il ${pos(locale, position)} ha vinto la Copa América.`;
|
||||
return `O ${pos(locale, position)} ganhou a Copa América.`;
|
||||
}
|
||||
|
||||
function tWonBallonDOr(locale: Locale, position: string): string {
|
||||
if (locale === "es") return `El ${pos(locale, position)} ha ganado el Balón de Oro.`;
|
||||
if (locale === "fr") return `Le ${pos(locale, position)} a remporté le Ballon d'Or.`;
|
||||
if (locale === "de") return `Der ${pos(locale, position)} hat den Ballon d'Or gewonnen.`;
|
||||
if (locale === "it") return `Il ${pos(locale, position)} ha vinto il Pallone d'Oro.`;
|
||||
return `O ${pos(locale, position)} ganhou a Bola de Ouro.`;
|
||||
}
|
||||
|
||||
function tWonGoldenBoots(locale: Locale, position: string, count: number): string {
|
||||
if (count === 1) {
|
||||
if (locale === "es") return `El ${pos(locale, position)} ha ganado la Bota de Oro europea.`;
|
||||
if (locale === "fr") return `Le ${pos(locale, position)} a remporté le Soulier d'Or européen.`;
|
||||
if (locale === "de") return `Der ${pos(locale, position)} hat den Europäischen Goldenen Schuh gewonnen.`;
|
||||
if (locale === "it") return `Il ${pos(locale, position)} ha vinto la Scarpa d'Oro europea.`;
|
||||
return `O ${pos(locale, position)} ganhou a Chuteira de Ouro europeia.`;
|
||||
}
|
||||
if (locale === "es") return `El ${pos(locale, position)} ha ganado ${count} Botas de Oro europeas.`;
|
||||
if (locale === "fr") return `Le ${pos(locale, position)} a remporté ${count} Souliers d'Or européens.`;
|
||||
if (locale === "de") return `Der ${pos(locale, position)} hat ${count} Europäische Goldene Schuhe gewonnen.`;
|
||||
if (locale === "it") return `Il ${pos(locale, position)} ha vinto ${count} Scarpe d'Oro europee.`;
|
||||
return `O ${pos(locale, position)} ganhou ${count} Chuteiras de Ouro europeias.`;
|
||||
}
|
||||
|
||||
function tPlayForSameClub(locale: Locale, left: string, right: string): string {
|
||||
if (locale === "es") return `El ${pos(locale, left)} y el ${pos(locale, right)} juegan en el mismo club.`;
|
||||
if (locale === "fr") return `Le ${pos(locale, left)} et le ${pos(locale, right)} jouent dans le même club.`;
|
||||
if (locale === "de") return `Der ${pos(locale, left)} und der ${pos(locale, right)} spielen beim selben Klub.`;
|
||||
if (locale === "it") return `Il ${pos(locale, left)} e il ${pos(locale, right)} giocano nello stesso club.`;
|
||||
return `O ${pos(locale, left)} e o ${pos(locale, right)} jogam no mesmo clube.`;
|
||||
}
|
||||
|
||||
+218
-165
@@ -15,11 +15,13 @@ const nationalityRegions = {
|
||||
Belgium: "Europe",
|
||||
Brazil: "South America",
|
||||
Canada: "North America",
|
||||
Croatia: "Europe",
|
||||
Egypt: "Africa",
|
||||
England: "Europe",
|
||||
France: "Europe",
|
||||
Georgia: "Europe",
|
||||
Germany: "Europe",
|
||||
Hungary: "Europe",
|
||||
Italy: "Europe",
|
||||
Netherlands: "Europe",
|
||||
Norway: "Europe",
|
||||
@@ -27,8 +29,24 @@ const nationalityRegions = {
|
||||
Portugal: "Europe",
|
||||
Spain: "Europe",
|
||||
Sweden: "Europe",
|
||||
Switzerland: "Europe",
|
||||
Ukraine: "Europe",
|
||||
};
|
||||
|
||||
// La Liga players and top globally-famous players are "easy" (0).
|
||||
// Premier League and other well-known players are "medium" (1).
|
||||
// Everyone else is "hard" (2).
|
||||
const EASY_PLAYER_IDS = new Set([
|
||||
"courtois", "vinicius", "mbappe", "bellingham", "rudiger", "lewandowski",
|
||||
"haaland", "salah", "de-bruyne", "van-dijk", "modric", "alisson", "ederson", "kane",
|
||||
]);
|
||||
|
||||
function playerDifficulty(player) {
|
||||
if (EASY_PLAYER_IDS.has(player.id) || player.league === "LaLiga") return 0;
|
||||
if (["Premier League", "Bundesliga", "Serie A", "Ligue 1"].includes(player.league)) return 1;
|
||||
return 2;
|
||||
}
|
||||
|
||||
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));
|
||||
@@ -91,47 +109,107 @@ function lineupFromIndex(index) {
|
||||
}
|
||||
|
||||
function buildClues(lineup, difficulty, fixedPosition) {
|
||||
const hiddenPositions = POSITIONS.filter((position) => position !== fixedPosition);
|
||||
const hiddenPositions = POSITIONS.filter((p) => p !== fixedPosition);
|
||||
|
||||
if (difficulty === "easy") {
|
||||
return limitClues(
|
||||
ensureCoverage(
|
||||
hiddenPositions.flatMap((position, index) => [
|
||||
clue(index % 2 === 0 ? buildCurrentLeagueClue(position, lineup[position]) : buildNationalityClue(position, lineup[position]), "identity"),
|
||||
clue(buildTitleCountOrCountryClue(lineup[position], subjectFor(position)), "titles"),
|
||||
]),
|
||||
hiddenPositions,
|
||||
),
|
||||
5,
|
||||
);
|
||||
}
|
||||
if (difficulty === "easy") return buildEasyClues(lineup, hiddenPositions);
|
||||
if (difficulty === "medium") return buildMediumClues(lineup, hiddenPositions);
|
||||
return buildHardClues(lineup, hiddenPositions);
|
||||
}
|
||||
|
||||
if (difficulty === "medium") {
|
||||
return limitClues(
|
||||
ensureCoverage(
|
||||
[
|
||||
...buildMediumPairClues(lineup, hiddenPositions),
|
||||
...hiddenPositions.map((position) => clue(buildNationalityRegionClue(position, lineup[position]), "nationality")),
|
||||
...hiddenPositions.map((position) => clue(buildCurrentLeagueClue(position, lineup[position]), "league")),
|
||||
...hiddenPositions.map((position) => clue(buildLeagueTitleBucketClue(lineup[position], subjectFor(position)), "titles")),
|
||||
],
|
||||
hiddenPositions,
|
||||
),
|
||||
5,
|
||||
);
|
||||
}
|
||||
// Easy: all 4 clues are specific and direct. Rotate through 4 distinct easy types.
|
||||
// Each position gets a different type so no repetition. A 5th title count clue is added.
|
||||
function buildEasyClues(lineup, hiddenPositions) {
|
||||
// Type rotation per position index — ensures diversity
|
||||
const typeBuilders = [
|
||||
(pos, player) => clue(buildCurrentLeagueClue(pos, player), "league"),
|
||||
(pos, player) => clue(buildNationalityClue(pos, player), "nationality"),
|
||||
(pos, player) => clue(buildClubClue(pos, player), "club"),
|
||||
(pos, player) => clue(buildTournamentWinnerClue(player, subjectFor(pos)) ?? buildCurrentLeagueClue(pos, player), "trophy"),
|
||||
];
|
||||
|
||||
const primaryClues = hiddenPositions.map((pos, i) => typeBuilders[i % typeBuilders.length](pos, lineup[pos]));
|
||||
|
||||
// Add title count and extra league/nationality clues as fill and backup coverage
|
||||
const fillClues = hiddenPositions.flatMap((pos) => {
|
||||
const player = lineup[pos];
|
||||
const subject = subjectFor(pos);
|
||||
return [
|
||||
clue(buildTitleCountOrCountryClue(player, subject), "titles"),
|
||||
clue(buildCurrentLeagueClue(pos, player), "league"),
|
||||
clue(buildNationalityClue(pos, player), "nationality"),
|
||||
];
|
||||
});
|
||||
|
||||
return limitClues(ensureCoverage(dedupeClues([...primaryClues, ...fillClues]), hiddenPositions), 5);
|
||||
}
|
||||
|
||||
// Medium: 2 positions get easy/direct clues, 2 get medium clues.
|
||||
// Sort positions by player difficulty — easiest players get direct clues.
|
||||
function buildMediumClues(lineup, hiddenPositions) {
|
||||
const sorted = [...hiddenPositions].sort((a, b) => playerDifficulty(lineup[a]) - playerDifficulty(lineup[b]));
|
||||
const easyPositions = sorted.slice(0, 2);
|
||||
const harderPositions = sorted.slice(2);
|
||||
|
||||
const easyClues = easyPositions.flatMap((pos) => {
|
||||
const player = lineup[pos];
|
||||
const subject = subjectFor(pos);
|
||||
return [
|
||||
clue(buildCurrentLeagueClue(pos, player), "league"),
|
||||
clue(buildNationalityClue(pos, player), "nationality"),
|
||||
clue(buildClubClue(pos, player), "club"),
|
||||
];
|
||||
});
|
||||
|
||||
const mediumClues = harderPositions.flatMap((pos) => {
|
||||
const player = lineup[pos];
|
||||
const subject = subjectFor(pos);
|
||||
return [
|
||||
clue(buildNationalityRegionClue(pos, player), "nationality"),
|
||||
clue(buildLeagueTitleBucketClue(player, subject), "titles"),
|
||||
clue(buildAwardClue(player, subject), "award"),
|
||||
clue(buildChampionsLeagueCountClue(player, subject), "champions"),
|
||||
clue(buildCurrentLeagueClue(pos, player), "league"),
|
||||
];
|
||||
});
|
||||
|
||||
// Pair clues add cross-position info
|
||||
const pairClues = buildMediumPairClues(lineup, hiddenPositions);
|
||||
|
||||
return limitClues(ensureCoverage(dedupeClues([...easyClues, ...mediumClues, ...pairClues]), hiddenPositions), 5);
|
||||
}
|
||||
|
||||
// Hard: 1 easy clue for the easiest player, 2 medium clues for the middle, hard comparisons for the rest.
|
||||
function buildHardClues(lineup, hiddenPositions) {
|
||||
const sorted = [...hiddenPositions].sort((a, b) => playerDifficulty(lineup[a]) - playerDifficulty(lineup[b]));
|
||||
const easiest = sorted[0];
|
||||
const middle = sorted.slice(1, 3);
|
||||
const hardest = sorted[sorted.length - 1];
|
||||
|
||||
const easyClueLine = [
|
||||
clue(buildNationalityClue(easiest, lineup[easiest]), "nationality"),
|
||||
];
|
||||
|
||||
const mediumLines = middle.flatMap((pos) => {
|
||||
const player = lineup[pos];
|
||||
const subject = subjectFor(pos);
|
||||
return [
|
||||
clue(buildNationalityRegionClue(pos, player), "nationality"),
|
||||
clue(buildLeagueFamilyClue(pos, player), "league"),
|
||||
clue(buildAwardClue(player, subject), "award"),
|
||||
clue(buildLeagueTitleBucketClue(player, subject), "titles"),
|
||||
];
|
||||
});
|
||||
|
||||
// Hardest player only gets comparisons — must piece together from context
|
||||
const hardLines = [
|
||||
clue(buildLeagueTitleBucketClue(lineup[hardest], subjectFor(hardest)), "titles"),
|
||||
clue(buildChampionsLeagueCountClue(lineup[hardest], subjectFor(hardest)), "champions"),
|
||||
];
|
||||
|
||||
const pairClues = buildHardPairClues(lineup, hiddenPositions);
|
||||
|
||||
return limitClues(
|
||||
ensureCoverage(
|
||||
[
|
||||
...buildHardPairClues(lineup, hiddenPositions),
|
||||
...hiddenPositions.map((position) => clue(buildNationalityRegionClue(position, lineup[position]), "nationality")),
|
||||
...hiddenPositions.map((position) => clue(buildLeagueFamilyClue(position, lineup[position]), "league")),
|
||||
...hiddenPositions.map((position) => clue(buildTitleCountryPairClue(lineup[position], subjectFor(position)), "titles")),
|
||||
...hiddenPositions.map((position) => clue(buildChampionsLeagueCountClue(lineup[position], subjectFor(position)), "champions")),
|
||||
],
|
||||
hiddenPositions,
|
||||
),
|
||||
ensureCoverage(dedupeClues([...easyClueLine, ...mediumLines, ...hardLines, ...pairClues]), hiddenPositions),
|
||||
6,
|
||||
);
|
||||
}
|
||||
@@ -140,6 +218,10 @@ function subjectFor(position) {
|
||||
return `The ${positionLabels[position]}`;
|
||||
}
|
||||
|
||||
function buildClubClue(position, player) {
|
||||
return `${subjectFor(position)} plays for ${player.club}.`;
|
||||
}
|
||||
|
||||
function buildNationalityClue(position, player) {
|
||||
return `${subjectFor(position)} is from ${player.nationality}.`;
|
||||
}
|
||||
@@ -161,12 +243,24 @@ function buildLeagueFamilyClue(position, player) {
|
||||
return `${subjectFor(position)} plays outside the top five leagues.`;
|
||||
}
|
||||
|
||||
function buildTitleCountryClue(player, subject) {
|
||||
const country = player.leagueTitleCountries?.[0];
|
||||
if (country) {
|
||||
return `${subject} has won league titles in ${country}.`;
|
||||
}
|
||||
return `${subject} has won ${player.leagueTitles} league titles.`;
|
||||
// Returns a specific tournament win clue, or null if the player has no major win.
|
||||
function buildTournamentWinnerClue(player, subject) {
|
||||
if (player.worldCupWinner) return `${subject} has won the World Cup.`;
|
||||
if (player.eurocupWinner) return `${subject} has won the European Championship.`;
|
||||
if (player.copaAmericaWinner) return `${subject} has won the Copa América.`;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Award clue: Ballon d'Or or Golden Boot count, or Champions League count as fallback.
|
||||
function buildAwardClue(player, subject) {
|
||||
if (player.ballonDOr > 0) return `${subject} has won the Ballon d'Or.`;
|
||||
if (player.goldenBoot > 1) return `${subject} has won ${player.goldenBoot} European Golden Boots.`;
|
||||
if (player.goldenBoot === 1) return `${subject} has won the European Golden Boot.`;
|
||||
return buildChampionsLeagueCountClue(player, subject);
|
||||
}
|
||||
|
||||
function buildChampionsLeagueCountClue(player, subject) {
|
||||
return `${subject} has won ${player.championsLeagueTitles} Champions League title${player.championsLeagueTitles === 1 ? "" : "s"}.`;
|
||||
}
|
||||
|
||||
function buildTitleCountOrCountryClue(player, subject) {
|
||||
@@ -176,81 +270,9 @@ function buildTitleCountOrCountryClue(player, subject) {
|
||||
return buildLeagueTitleCountClue(player, subject);
|
||||
}
|
||||
|
||||
function buildChampionsLeagueCountClue(player, subject) {
|
||||
return `${subject} has won ${player.championsLeagueTitles} Champions League title${player.championsLeagueTitles === 1 ? "" : "s"}.`;
|
||||
}
|
||||
|
||||
function buildNumericLeagueClue(player, subject) {
|
||||
return buildLeagueTitleCountClue(player, subject);
|
||||
}
|
||||
|
||||
function buildFormerClubClue(playerA, playerB, subject) {
|
||||
const common = (playerA.formerClubs ?? []).find((club) => (playerB.formerClubs ?? []).includes(club));
|
||||
if (common) {
|
||||
return `${subject} were once teammates at ${common}.`;
|
||||
}
|
||||
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 buildHiddenPairClues(lineup, hiddenPositions) {
|
||||
const clues = [];
|
||||
for (let leftIndex = 0; leftIndex < hiddenPositions.length; leftIndex += 1) {
|
||||
for (let rightIndex = leftIndex + 1; rightIndex < hiddenPositions.length; rightIndex += 1) {
|
||||
const left = hiddenPositions[leftIndex];
|
||||
const right = hiddenPositions[rightIndex];
|
||||
clues.push(clue(buildFormerClubClue(lineup[left], lineup[right], `${subjectFor(left)} and ${positionLabels[right]}`), "history"));
|
||||
clues.push(clue(buildLeagueTitleComparisonClue(left, lineup[left], right, lineup[right]), "comparison"));
|
||||
clues.push(clue(buildChampionsLeagueComparisonClue(left, lineup[left], right, lineup[right]), "champions"));
|
||||
}
|
||||
}
|
||||
return dedupeClues(clues);
|
||||
}
|
||||
|
||||
function buildMediumPairClues(lineup, hiddenPositions) {
|
||||
const pairs = [];
|
||||
for (let leftIndex = 0; leftIndex < hiddenPositions.length; leftIndex += 1) {
|
||||
for (let rightIndex = leftIndex + 1; rightIndex < hiddenPositions.length; rightIndex += 1) {
|
||||
const left = hiddenPositions[leftIndex];
|
||||
const right = hiddenPositions[rightIndex];
|
||||
pairs.push(clue(buildFormerClubClue(lineup[left], lineup[right], `${subjectFor(left)} and ${positionLabels[right]}`), "history"));
|
||||
pairs.push(clue(buildLeagueTitleComparisonClue(left, lineup[left], right, lineup[right]), "comparison"));
|
||||
}
|
||||
}
|
||||
return dedupeClues(pairs);
|
||||
}
|
||||
|
||||
function buildHardPairClues(lineup, hiddenPositions) {
|
||||
const pairs = [];
|
||||
for (let leftIndex = 0; leftIndex < hiddenPositions.length; leftIndex += 1) {
|
||||
for (let rightIndex = leftIndex + 1; rightIndex < hiddenPositions.length; rightIndex += 1) {
|
||||
const left = hiddenPositions[leftIndex];
|
||||
const right = hiddenPositions[rightIndex];
|
||||
pairs.push(clue(buildFormerClubClue(lineup[left], lineup[right], `${subjectFor(left)} and ${positionLabels[right]}`), "history"));
|
||||
pairs.push(clue(buildChampionsLeagueComparisonClue(left, lineup[left], right, lineup[right]), "champions"));
|
||||
pairs.push(clue(buildLeagueTitleComparisonClue(left, lineup[left], right, lineup[right]), "comparison"));
|
||||
}
|
||||
}
|
||||
return dedupeClues(pairs);
|
||||
}
|
||||
|
||||
function buildTitleCountryPairClue(player, subject) {
|
||||
const countries = player.leagueTitleCountries ?? [];
|
||||
if (countries.length >= 2) {
|
||||
return `${subject} has won league titles in ${countries.slice(0, 2).join(" and ")}.`;
|
||||
}
|
||||
if (countries.length === 1) {
|
||||
return `${subject} has won league titles in ${countries[0]}.`;
|
||||
}
|
||||
function buildTitleCountryClue(player, subject) {
|
||||
const country = player.leagueTitleCountries?.[0];
|
||||
if (country) return `${subject} has won league titles in ${country}.`;
|
||||
return buildLeagueTitleCountClue(player, subject);
|
||||
}
|
||||
|
||||
@@ -265,12 +287,32 @@ function buildLeagueTitleBucketClue(player, subject) {
|
||||
return `${subject} has won more than 5 league titles.`;
|
||||
}
|
||||
|
||||
function buildTitleCountryPairClue(player, subject) {
|
||||
const countries = player.leagueTitleCountries ?? [];
|
||||
if (countries.length >= 2) return `${subject} has won league titles in ${countries.slice(0, 2).join(" and ")}.`;
|
||||
if (countries.length === 1) return `${subject} has won league titles in ${countries[0]}.`;
|
||||
return buildLeagueTitleCountClue(player, subject);
|
||||
}
|
||||
|
||||
function buildFormerClubClue(playerA, playerB, subject) {
|
||||
const common = (playerA.formerClubs ?? []).find((club) => (playerB.formerClubs ?? []).includes(club));
|
||||
if (common) return `${subject} were once teammates at ${common}.`;
|
||||
if (playerA.club === playerB.club) return `${subject} play for the same club.`;
|
||||
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 buildLeagueTitleComparisonClue(positionA, playerA, positionB, playerB) {
|
||||
const subjectA = subjectFor(positionA);
|
||||
const subjectB = `the ${positionLabels[positionB]}`;
|
||||
if (playerA.leagueTitles > playerB.leagueTitles) {
|
||||
return `${subjectA} has more league titles than ${subjectB}.`;
|
||||
}
|
||||
if (playerA.leagueTitles > playerB.leagueTitles) return `${subjectA} has more league titles than ${subjectB}.`;
|
||||
if (playerB.leagueTitles > playerA.leagueTitles) {
|
||||
return `${subjectFor(positionB)} has more league titles than the ${positionLabels[positionA]}.`;
|
||||
}
|
||||
@@ -289,6 +331,35 @@ function buildChampionsLeagueComparisonClue(positionA, playerA, positionB, playe
|
||||
return `${subjectA} and ${positionLabels[positionB]} have won the same number of Champions League titles.`;
|
||||
}
|
||||
|
||||
function buildMediumPairClues(lineup, hiddenPositions) {
|
||||
const pairs = [];
|
||||
for (let l = 0; l < hiddenPositions.length; l += 1) {
|
||||
for (let r = l + 1; r < hiddenPositions.length; r += 1) {
|
||||
const left = hiddenPositions[l];
|
||||
const right = hiddenPositions[r];
|
||||
const subject = `${subjectFor(left)} and ${positionLabels[right]}`;
|
||||
pairs.push(clue(buildFormerClubClue(lineup[left], lineup[right], subject), "history"));
|
||||
pairs.push(clue(buildLeagueTitleComparisonClue(left, lineup[left], right, lineup[right]), "comparison"));
|
||||
}
|
||||
}
|
||||
return dedupeClues(pairs);
|
||||
}
|
||||
|
||||
function buildHardPairClues(lineup, hiddenPositions) {
|
||||
const pairs = [];
|
||||
for (let l = 0; l < hiddenPositions.length; l += 1) {
|
||||
for (let r = l + 1; r < hiddenPositions.length; r += 1) {
|
||||
const left = hiddenPositions[l];
|
||||
const right = hiddenPositions[r];
|
||||
const subject = `${subjectFor(left)} and ${positionLabels[right]}`;
|
||||
pairs.push(clue(buildFormerClubClue(lineup[left], lineup[right], subject), "history"));
|
||||
pairs.push(clue(buildChampionsLeagueComparisonClue(left, lineup[left], right, lineup[right]), "champions"));
|
||||
pairs.push(clue(buildLeagueTitleComparisonClue(left, lineup[left], right, lineup[right]), "comparison"));
|
||||
}
|
||||
}
|
||||
return dedupeClues(pairs);
|
||||
}
|
||||
|
||||
function dedupeClues(clues) {
|
||||
return [...new Map(clues.map((item) => [item.text, item])).values()];
|
||||
}
|
||||
@@ -297,65 +368,62 @@ function ensureCoverage(clues, hiddenPositions) {
|
||||
const selected = [];
|
||||
const covered = new Set();
|
||||
const usedTypes = new Set();
|
||||
const positionCounts = Object.fromEntries(hiddenPositions.map((position) => [position, 0]));
|
||||
const positionCounts = Object.fromEntries(hiddenPositions.map((p) => [p, 0]));
|
||||
const normalizedClues = clues.map((item) => (typeof item === "string" ? clue(item, "general") : item));
|
||||
|
||||
while (covered.size < hiddenPositions.length) {
|
||||
const nextClue = normalizedClues
|
||||
.filter((candidate) => !selected.includes(candidate))
|
||||
.map((candidate) => ({
|
||||
clue: candidate,
|
||||
positions: mentionedPositions(candidate.text, hiddenPositions),
|
||||
}))
|
||||
.filter((candidate) => candidate.positions.some((position) => !covered.has(position)))
|
||||
.sort((left, right) => {
|
||||
const leftNew = left.positions.filter((position) => !covered.has(position)).length;
|
||||
const rightNew = right.positions.filter((position) => !covered.has(position)).length;
|
||||
const leftTypeBonus = usedTypes.has(left.clue.type) ? 0 : 1;
|
||||
const rightTypeBonus = usedTypes.has(right.clue.type) ? 0 : 1;
|
||||
return rightNew - leftNew || rightTypeBonus - leftTypeBonus || left.positions.length - right.positions.length;
|
||||
.filter((c) => !selected.includes(c))
|
||||
.map((c) => ({ clue: c, positions: mentionedPositions(c.text, hiddenPositions) }))
|
||||
.filter((c) => c.positions.some((p) => !covered.has(p)))
|
||||
.sort((l, r) => {
|
||||
const lNew = l.positions.filter((p) => !covered.has(p)).length;
|
||||
const rNew = r.positions.filter((p) => !covered.has(p)).length;
|
||||
const lTypeBonus = usedTypes.has(l.clue.type) ? 0 : 1;
|
||||
const rTypeBonus = usedTypes.has(r.clue.type) ? 0 : 1;
|
||||
return rNew - lNew || rTypeBonus - lTypeBonus || l.positions.length - r.positions.length;
|
||||
})[0]?.clue;
|
||||
|
||||
if (!nextClue) break;
|
||||
selected.push(nextClue);
|
||||
usedTypes.add(nextClue.type);
|
||||
for (const position of mentionedPositions(nextClue.text, hiddenPositions)) {
|
||||
covered.add(position);
|
||||
positionCounts[position] += 1;
|
||||
for (const p of mentionedPositions(nextClue.text, hiddenPositions)) {
|
||||
covered.add(p);
|
||||
positionCounts[p] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
while (selected.length < normalizedClues.length) {
|
||||
const nextClue = normalizedClues
|
||||
.filter((candidate) => !selected.includes(candidate))
|
||||
.sort((left, right) => {
|
||||
const leftTypeBonus = usedTypes.has(left.type) ? 0 : 1;
|
||||
const rightTypeBonus = usedTypes.has(right.type) ? 0 : 1;
|
||||
const leftBalance = clueBalanceScore(left.text, hiddenPositions, positionCounts);
|
||||
const rightBalance = clueBalanceScore(right.text, hiddenPositions, positionCounts);
|
||||
return rightTypeBonus - leftTypeBonus || rightBalance - leftBalance;
|
||||
.filter((c) => !selected.includes(c))
|
||||
.sort((l, r) => {
|
||||
const lTypeBonus = usedTypes.has(l.type) ? 0 : 1;
|
||||
const rTypeBonus = usedTypes.has(r.type) ? 0 : 1;
|
||||
const lBalance = clueBalanceScore(l.text, hiddenPositions, positionCounts);
|
||||
const rBalance = clueBalanceScore(r.text, hiddenPositions, positionCounts);
|
||||
return rTypeBonus - lTypeBonus || rBalance - lBalance;
|
||||
})[0];
|
||||
|
||||
if (!nextClue) break;
|
||||
selected.push(nextClue);
|
||||
usedTypes.add(nextClue.type);
|
||||
for (const position of mentionedPositions(nextClue.text, hiddenPositions)) {
|
||||
positionCounts[position] += 1;
|
||||
for (const p of mentionedPositions(nextClue.text, hiddenPositions)) {
|
||||
positionCounts[p] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return dedupeClues(selected).map((item) => item.text);
|
||||
}
|
||||
|
||||
function mentionedPositions(clue, hiddenPositions) {
|
||||
const normalized = clue.toLowerCase();
|
||||
return hiddenPositions.filter((position) => normalized.includes(positionLabels[position]));
|
||||
function mentionedPositions(clueText, hiddenPositions) {
|
||||
const normalized = clueText.toLowerCase();
|
||||
return hiddenPositions.filter((p) => normalized.includes(positionLabels[p]));
|
||||
}
|
||||
|
||||
function clueBalanceScore(clueText, hiddenPositions, positionCounts) {
|
||||
const positions = mentionedPositions(clueText, hiddenPositions);
|
||||
if (positions.length === 0) return -99;
|
||||
return -positions.reduce((sum, position) => sum + positionCounts[position], 0) / positions.length;
|
||||
return -positions.reduce((sum, p) => sum + positionCounts[p], 0) / positions.length;
|
||||
}
|
||||
|
||||
function limitClues(clues, max) {
|
||||
@@ -371,18 +439,3 @@ function addDays(dateKey, daysToAdd) {
|
||||
date.setUTCDate(date.getUTCDate() + daysToAdd);
|
||||
return date.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function getTodayKeyInTimeZone(date, timeZone) {
|
||||
const formatter = new Intl.DateTimeFormat("en-CA", {
|
||||
timeZone,
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
});
|
||||
|
||||
const parts = formatter.formatToParts(date);
|
||||
const year = parts.find((part) => part.type === "year")?.value;
|
||||
const month = parts.find((part) => part.type === "month")?.value;
|
||||
const day = parts.find((part) => part.type === "day")?.value;
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
@@ -109,11 +109,14 @@ for (const puzzle of puzzles) {
|
||||
console.log("Puzzle data OK");
|
||||
|
||||
function classifyClue(clue) {
|
||||
if (clue.includes("teammates")) return "history";
|
||||
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";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user