Reduce clue volume and tune difficulty
Build and push image / build (push) Successful in 1m19s

This commit is contained in:
alexandrev-tibco
2026-05-08 14:12:49 +02:00
parent f784c9c722
commit 990fd641fe
4 changed files with 3480 additions and 6345 deletions
+3311 -6332
View File
File diff suppressed because it is too large Load Diff
+52
View File
@@ -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
View File
@@ -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);
+4
View File
@@ -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}"`);