This commit is contained in:
+3311
-6332
File diff suppressed because it is too large
Load Diff
+52
@@ -710,6 +710,26 @@ function translateGeneratedClue(clue: string, locale: Locale): string {
|
||||
return tWonLeagueTitles(locale, wonLeagueTitles[1], Number(wonLeagueTitles[2]));
|
||||
}
|
||||
|
||||
const noLeagueTitle = clue.match(/^The (.+) has not won a league title\.$/);
|
||||
if (noLeagueTitle) {
|
||||
return tNoLeagueTitle(locale, noLeagueTitle[1]);
|
||||
}
|
||||
|
||||
const atMostLeagueTitles = clue.match(/^The (.+) has won at most 2 league titles\.$/);
|
||||
if (atMostLeagueTitles) {
|
||||
return tAtMostLeagueTitles(locale, atMostLeagueTitles[1], 2);
|
||||
}
|
||||
|
||||
const betweenLeagueTitles = clue.match(/^The (.+) has won between 3 and 5 league titles\.$/);
|
||||
if (betweenLeagueTitles) {
|
||||
return tBetweenLeagueTitles(locale, betweenLeagueTitles[1], 3, 5);
|
||||
}
|
||||
|
||||
const moreThanLeagueTitles = clue.match(/^The (.+) has won more than 5 league titles\.$/);
|
||||
if (moreThanLeagueTitles) {
|
||||
return tMoreThanLeagueTitles(locale, moreThanLeagueTitles[1], 5);
|
||||
}
|
||||
|
||||
const wonChampionsTitles = clue.match(/^The (.+) has won (\d+) Champions League titles?\.$/);
|
||||
if (wonChampionsTitles) {
|
||||
return tWonChampionsTitles(locale, wonChampionsTitles[1], Number(wonChampionsTitles[2]));
|
||||
@@ -942,6 +962,38 @@ function tWonLeagueTitles(locale: Locale, position: string, count: number): stri
|
||||
return `O ${pos(locale, position)} ganhou ${count} ${count === 1 ? "campeonato" : "campeonatos"}.`;
|
||||
}
|
||||
|
||||
function tNoLeagueTitle(locale: Locale, position: string): string {
|
||||
if (locale === "es") return `El ${pos(locale, position)} no ha ganado ninguna liga.`;
|
||||
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.`;
|
||||
return `O ${pos(locale, position)} não ganhou nenhum campeonato.`;
|
||||
}
|
||||
|
||||
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 === "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.`;
|
||||
return `O ${pos(locale, position)} ganhou no máximo ${count} campeonatos.`;
|
||||
}
|
||||
|
||||
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 === "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.`;
|
||||
return `O ${pos(locale, position)} ganhou entre ${min} e ${max} campeonatos.`;
|
||||
}
|
||||
|
||||
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 === "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.`;
|
||||
return `O ${pos(locale, position)} ganhou mais de ${count} campeonatos.`;
|
||||
}
|
||||
|
||||
function tWonChampionsTitles(locale: Locale, position: string, count: number): string {
|
||||
if (locale === "es") return `El ${pos(locale, position)} ha ganado ${count} ${count === 1 ? "Champions League" : "Champions League"}.`;
|
||||
if (locale === "fr") return `Le ${pos(locale, position)} a gagné ${count} ${count === 1 ? "Ligue des champions" : "Ligues des champions"}.`;
|
||||
|
||||
+113
-13
@@ -75,24 +75,42 @@ function buildClues(lineup, difficulty, fixedPosition) {
|
||||
const hiddenPositions = POSITIONS.filter((position) => position !== fixedPosition);
|
||||
|
||||
if (difficulty === "easy") {
|
||||
return hiddenPositions.flatMap((position) => [
|
||||
buildNationalityClue(position, lineup[position]),
|
||||
buildCurrentLeagueClue(position, lineup[position]),
|
||||
]);
|
||||
return limitClues(
|
||||
ensureCoverage(
|
||||
hiddenPositions.flatMap((position, index) => [
|
||||
index % 2 === 0 ? buildCurrentLeagueClue(position, lineup[position]) : buildNationalityClue(position, lineup[position]),
|
||||
buildTitleCountOrCountryClue(lineup[position], subjectFor(position)),
|
||||
]),
|
||||
hiddenPositions,
|
||||
),
|
||||
5,
|
||||
);
|
||||
}
|
||||
|
||||
if (difficulty === "medium") {
|
||||
return dedupeClues([
|
||||
...hiddenPositions.map((position) => buildTitleCountryClue(lineup[position], subjectFor(position))),
|
||||
...buildHiddenPairClues(lineup, hiddenPositions).slice(0, 2),
|
||||
]);
|
||||
return limitClues(
|
||||
ensureCoverage(
|
||||
[
|
||||
...buildMediumPairClues(lineup, hiddenPositions),
|
||||
...hiddenPositions.map((position) => buildLeagueTitleBucketClue(lineup[position], subjectFor(position))),
|
||||
],
|
||||
hiddenPositions,
|
||||
),
|
||||
5,
|
||||
);
|
||||
}
|
||||
|
||||
return dedupeClues([
|
||||
...hiddenPositions.map((position) => buildTitleCountryPairClue(lineup[position], subjectFor(position))),
|
||||
...hiddenPositions.map((position) => buildChampionsLeagueCountClue(lineup[position], subjectFor(position))),
|
||||
...buildHiddenPairClues(lineup, hiddenPositions).slice(0, 3),
|
||||
]);
|
||||
return limitClues(
|
||||
ensureCoverage(
|
||||
[
|
||||
...buildHardPairClues(lineup, hiddenPositions),
|
||||
...hiddenPositions.map((position) => buildTitleCountryPairClue(lineup[position], subjectFor(position))),
|
||||
...hiddenPositions.map((position) => buildChampionsLeagueCountClue(lineup[position], subjectFor(position))),
|
||||
],
|
||||
hiddenPositions,
|
||||
),
|
||||
6,
|
||||
);
|
||||
}
|
||||
|
||||
function subjectFor(position) {
|
||||
@@ -115,6 +133,13 @@ function buildTitleCountryClue(player, subject) {
|
||||
return `${subject} has won ${player.leagueTitles} league titles.`;
|
||||
}
|
||||
|
||||
function buildTitleCountOrCountryClue(player, subject) {
|
||||
if ((player.leagueTitleCountries?.length ?? 0) > 0 && player.leagueTitles > 2) {
|
||||
return buildTitleCountryClue(player, subject);
|
||||
}
|
||||
return buildLeagueTitleCountClue(player, subject);
|
||||
}
|
||||
|
||||
function buildChampionsLeagueCountClue(player, subject) {
|
||||
return `${subject} has won ${player.championsLeagueTitles} Champions League title${player.championsLeagueTitles === 1 ? "" : "s"}.`;
|
||||
}
|
||||
@@ -155,6 +180,33 @@ function buildHiddenPairClues(lineup, hiddenPositions) {
|
||||
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(buildFormerClubClue(lineup[left], lineup[right], `${subjectFor(left)} and ${positionLabels[right]}`));
|
||||
pairs.push(buildLeagueTitleComparisonClue(left, lineup[left], right, lineup[right]));
|
||||
}
|
||||
}
|
||||
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(buildFormerClubClue(lineup[left], lineup[right], `${subjectFor(left)} and ${positionLabels[right]}`));
|
||||
pairs.push(buildChampionsLeagueComparisonClue(left, lineup[left], right, lineup[right]));
|
||||
pairs.push(buildLeagueTitleComparisonClue(left, lineup[left], right, lineup[right]));
|
||||
}
|
||||
}
|
||||
return dedupeClues(pairs);
|
||||
}
|
||||
|
||||
function buildTitleCountryPairClue(player, subject) {
|
||||
const countries = player.leagueTitleCountries ?? [];
|
||||
if (countries.length >= 2) {
|
||||
@@ -170,6 +222,13 @@ function buildLeagueTitleCountClue(player, subject) {
|
||||
return `${subject} has won ${player.leagueTitles} league title${player.leagueTitles === 1 ? "" : "s"}.`;
|
||||
}
|
||||
|
||||
function buildLeagueTitleBucketClue(player, subject) {
|
||||
if (player.leagueTitles === 0) return `${subject} has not won a league title.`;
|
||||
if (player.leagueTitles <= 2) return `${subject} has won at most 2 league titles.`;
|
||||
if (player.leagueTitles <= 5) return `${subject} has won between 3 and 5 league titles.`;
|
||||
return `${subject} has won more than 5 league titles.`;
|
||||
}
|
||||
|
||||
function buildLeagueTitleComparisonClue(positionA, playerA, positionB, playerB) {
|
||||
const subjectA = subjectFor(positionA);
|
||||
const subjectB = `the ${positionLabels[positionB]}`;
|
||||
@@ -198,6 +257,47 @@ function dedupeClues(clues) {
|
||||
return [...new Set(clues)];
|
||||
}
|
||||
|
||||
function ensureCoverage(clues, hiddenPositions) {
|
||||
const selected = [];
|
||||
const covered = new Set();
|
||||
|
||||
while (covered.size < hiddenPositions.length) {
|
||||
const clue = clues
|
||||
.filter((candidate) => !selected.includes(candidate))
|
||||
.map((candidate) => ({
|
||||
clue: candidate,
|
||||
positions: mentionedPositions(candidate, 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;
|
||||
return rightNew - leftNew || left.positions.length - right.positions.length;
|
||||
})[0]?.clue;
|
||||
|
||||
if (!clue) break;
|
||||
selected.push(clue);
|
||||
for (const position of mentionedPositions(clue, hiddenPositions)) {
|
||||
covered.add(position);
|
||||
}
|
||||
}
|
||||
|
||||
for (const clue of clues) {
|
||||
if (!selected.includes(clue)) selected.push(clue);
|
||||
}
|
||||
|
||||
return dedupeClues(selected);
|
||||
}
|
||||
|
||||
function mentionedPositions(clue, hiddenPositions) {
|
||||
const normalized = clue.toLowerCase();
|
||||
return hiddenPositions.filter((position) => normalized.includes(positionLabels[position]));
|
||||
}
|
||||
|
||||
function limitClues(clues, max) {
|
||||
return clues.slice(0, max);
|
||||
}
|
||||
|
||||
function addDays(dateKey, daysToAdd) {
|
||||
const date = new Date(`${dateKey}T00:00:00.000Z`);
|
||||
date.setUTCDate(date.getUTCDate() + daysToAdd);
|
||||
|
||||
@@ -53,6 +53,10 @@ for (const puzzle of puzzles) {
|
||||
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`);
|
||||
}
|
||||
|
||||
for (const clue of puzzle.clues) {
|
||||
if (forbiddenClueFragments.some((fragment) => clue.includes(fragment))) {
|
||||
throw new Error(`${puzzle.id}: forbidden clue "${clue}"`);
|
||||
|
||||
Reference in New Issue
Block a user