From a8d950ce02f3b91a964fcf98f16ca34b915dde1b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Nov 2026 21:17:47 +0000 Subject: [PATCH] =?UTF-8?q?Lligues=20dels=20Pa=C3=AFsos=20Catalans=20sense?= =?UTF-8?q?=20noms=20castellans?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - nomsLliga.js tornat a copiar d'El Tauler: la barreja d'altres orígens de cada territori ja no té noms castellans ni de l'Amèrica hispana (la seva part són noms del territori), i els noms valencians i de la Franja ja no tenen formes castellanes. - catalanRivals: una temporada començada amb noms castellans els canvia per catalans del territori en carregar-la (sempre els mateixos). - A Europa, al món i per a qui juga des de fora, els noms del país. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0135ikkUjsY7jDihSMn6EBta --- AGENT.md | 20 +++++ apps/web/src/lliga/league.test.ts | 50 +++++++++++- apps/web/src/lliga/league.ts | 23 +++++- apps/web/src/lliga/nomsLliga.d.ts | 27 +++++++ apps/web/src/lliga/nomsLliga.js | 127 ++++++++++++++++++++++++------ docs/LLIGA.md | 14 +++- 6 files changed, 235 insertions(+), 26 deletions(-) diff --git a/AGENT.md b/AGENT.md index 9e79773..9ecca12 100644 --- a/AGENT.md +++ b/AGENT.md @@ -2509,3 +2509,23 @@ que diuen les regles del Rummikub.» desats d'abans, temporada començada que conserva les jornades) i del repte acabat; la prova de navegador de la temporada sencera comprova que no hi ha comptador i que les voltes canvien les jornades (6 → 3). + +### Lligues dels Països Catalans sense noms castellans ✅ Feta (2026-11-14) + +Demanat pel jugador: «Substitueix els noms i cognoms castellans de totes les +lligues dins dels Països Catalans, des de les locals, a Remigi i El Tauler.» + +- [x] `nomsLliga.js` tornat a copiar d'El Tauler (segueix/eltauler, + `nomslliga.js`): la barreja d'altres orígens de cada territori ja no té + noms castellans ni de l'Amèrica hispana (la seva part passa als noms del + territori), i els noms valencians i de la Franja ja no tenen formes + castellanes. Nous: `isCastilianName`, `catalanRivalName`, `CATALAN_FORMS`. +- [x] `catalanRivals` (`league.ts`): una temporada començada amb noms + castellans els canvia per catalans del territori en carregar-la, sempre + els mateixos (llavor de la temporada); els resultats van per identificador. + +**Criteris d'acceptació (verificats)**: cap nom castellà ni forma castellana +del graó 0 al 14 des dels set territoris (20 llavors per graó); una temporada +desada amb noms castellans es carrega amb noms catalans, la resta igual i de +manera determinista; a Europa no es toca. El castellà es manté a Europa, al +món i per a qui juga des de fora. diff --git a/apps/web/src/lliga/league.test.ts b/apps/web/src/lliga/league.test.ts index 9d4886f..8e21e27 100644 --- a/apps/web/src/lliga/league.test.ts +++ b/apps/web/src/lliga/league.test.ts @@ -44,7 +44,8 @@ import { zoneSize, type LeagueSeason, } from './league'; -import type { LeagueHome } from './localitats'; +import { normalizeHome, type LeagueHome } from './localitats'; +import { CATALAN_FORMS, isCastilianName } from './nomsLliga'; const VIC: LeagueHome = { territory: 'cat', municipi: 'Vic', comarca: 'Osona', provincia: 'Barcelona', pais: '' }; @@ -216,6 +217,53 @@ describe('els rivals', () => { expect(a).toEqual(b); }); + it('cap nom castellà del municipi als Països Catalans, des de cap territori', () => { + const homes = ( + [['cat', 'Vic'], ['val', 'Gandia'], ['bal', 'Palma'], ['and', 'Andorra la Vella'], ['nord', 'Perpinyà'], + ['franja', 'Fraga'], ['alguer', "l'Alguer"]] as const + ).map(([territory, municipi]) => normalizeHome({ territory, municipi })); + const forms = new Set(Object.keys(CATALAN_FORMS)); + const config = { opponents: 2 as const, players: 6, legs: 1 as const, rounds: 3 }; + for (const home of homes) { + // Del graó 0 (Municipal C) al 14 (Països Catalans A). + for (let tier = 0; tier <= 14; tier++) { + for (let seed = 1; seed <= 20; seed++) { + for (const rival of createSeason({ tier, config, home, seed }).rivals) { + expect(isCastilianName(rival.name)).toBe(false); + for (const word of rival.name.split(' ')) expect(forms.has(word)).toBe(false); + } + } + } + } + }, 30_000); + + it('una temporada començada amb noms castellans, en carregar-la, els passa a català', () => { + const season = recordRound(createSeason({ tier: TERRITORIAL_TIER, config: DEFAULT_CONFIG, home: VIC, seed: 5 }), [40, -10, -10, -20]); + const old = { + ...season, + rivals: season.rivals.map((r, i) => (i === 0 ? { ...r, name: 'Lucía Navarro' } : i === 1 ? { ...r, name: 'Carmen Ferrer' } : r)), + }; + const raw = JSON.stringify({ career: newCareer(VIC), season: old }); + const parsed = parseLeagueState(raw).season!; + expect(isCastilianName(parsed.rivals[0].name)).toBe(false); + expect(parsed.rivals[0].name).toMatch(/^\S+ \S+/); + expect(parsed.rivals[1].name).toBe('Carme Ferrer'); + // La resta, els identificadors i els resultats, tal com eren. + expect(parsed.rivals.slice(2)).toEqual(season.rivals.slice(2)); + expect(parsed.rivals.map((r) => r.id)).toEqual(season.rivals.map((r) => r.id)); + expect(parsed.results).toEqual(season.results); + expect(new Set(parsed.rivals.map((r) => r.name)).size).toBe(parsed.rivals.length); + // El nom nou no repeteix el cognom de cap altre rival. + const surname = (name: string) => name.split(' ').slice(1).join(' '); + expect(parsed.rivals.slice(1).map((r) => surname(r.name))).not.toContain(surname(parsed.rivals[0].name)); + // Cada càrrega, els mateixos noms. + expect(parseLeagueState(raw).season!.rivals).toEqual(parsed.rivals); + // A Europa (graó 15) no es toca. + const europe = createSeason({ tier: 15, config: DEFAULT_CONFIG, home: VIC, seed: 5 }); + const spanish = { ...europe, rivals: europe.rivals.map((r, i) => (i === 0 ? { ...r, name: 'Lucía Navarro' } : r)) }; + expect(parseLeagueState(JSON.stringify({ career: newCareer(VIC), season: spanish })).season!.rivals[0].name).toBe('Lucía Navarro'); + }); + it('no repeteix els noms de les temporades anteriors', () => { const first = createSeason({ tier: 0, config: DEFAULT_CONFIG, home: VIC, seed: 3 }); const exclude = first.rivals.map((r) => r.name); diff --git a/apps/web/src/lliga/league.ts b/apps/web/src/lliga/league.ts index 5caffe0..f516162 100644 --- a/apps/web/src/lliga/league.ts +++ b/apps/web/src/lliga/league.ts @@ -8,7 +8,7 @@ import { type Rng, } from '@remigi/core'; import { islandGroup, originTerritory, rivalOrigins, type LeagueHome } from './localitats'; -import { rivalNames } from './nomsLliga'; +import { catalanRivalName, fold, rivalNames } from './nomsLliga'; /** * La lliga de Remigi (docs/LLIGA.md): la mateixa escala que les lligues @@ -997,6 +997,7 @@ export function parseLeagueState(raw: string | null): LeagueState { const season = validSeason(value.season); // Els noms d'abans («· Grup B», «· Lliga B») amb el d'ara («· Divisió B»). if (season) season.name = renameDivision(season.name); + if (season) season.rivals = catalanRivals(season, career.home); career.seasons = career.seasons.map((record) => ({ ...record, name: renameDivision(record.name) })); return { career, season }; } catch { @@ -1004,6 +1005,26 @@ export function parseLeagueState(raw: string | null): LeagueState { } } +/** + * Rivals d'una temporada començada quan encara hi sortien noms castellans: a + * les lligues dels Països Catalans (del municipi als Països Catalans) passen a + * tenir nom català del seu territori (`catalanRivalName`). Els resultats van + * per identificador, així que no cal tocar res més. La llavor és la de la + * temporada: cada càrrega dona els mateixos noms. + */ +export function catalanRivals(season: LeagueSeason, home: LeagueHome | null): LeagueRival[] { + const level = tierLevel(season.tier); + const options = { + random: createRng((season.seed ^ 0x9e3779b9) >>> 0), + taken: new Set(season.rivals.map((rival) => fold(rival.name))), + territoryOf: originTerritory, + }; + return season.rivals.map((rival) => { + const name = catalanRivalName(rival.name, level, home, rival.origin, options); + return name === rival.name ? rival : { ...rival, name }; + }); +} + export function serializeLeagueState(state: LeagueState): string { return JSON.stringify(state); } diff --git a/apps/web/src/lliga/nomsLliga.d.ts b/apps/web/src/lliga/nomsLliga.d.ts index 59541ef..5374c1a 100644 --- a/apps/web/src/lliga/nomsLliga.d.ts +++ b/apps/web/src/lliga/nomsLliga.d.ts @@ -35,3 +35,30 @@ export function rivalNames( territoryOf?: (city: string) => TerritoryId | null; }, ): string[]; +/** Un nom fet amb un nom i un cognom castellans (García, Martínez...). */ +export function isCastilianName(name: string): boolean; +/** + * El nom d'un rival d'una temporada ja començada, sense res de castellà a les + * lligues dels Països Catalans (nivells 0-4): un nom castellà en dona un de nou + * del territori (cap dels de `taken`, que s'hi afegeix) i les formes + * castellanes d'abans passen a la catalana. A Europa, al món i per a qui juga + * des de fora, el nom no es toca. + */ +export function catalanRivalName( + name: string, + level: number, + home: LeagueHome | null, + origin: string | null, + options?: { + random?: () => number; + taken?: Set; + territoryOf?: (city: string) => TerritoryId | null; + }, +): string; +/** Formes castellanes d'abans entre els noms dels territoris → la catalana. */ +export const CATALAN_FORMS: Readonly>; +/** Cultures del castellà, que no surten a les lligues dels Països Catalans. */ +export const CASTILIAN_CULTURES: readonly string[]; +export const DIVERSE: Readonly>; +export const TERRITORY_CULTURE: Readonly>; +export const CULTURES: Readonly>; diff --git a/apps/web/src/lliga/nomsLliga.js b/apps/web/src/lliga/nomsLliga.js index 21e0804..3f490d9 100644 --- a/apps/web/src/lliga/nomsLliga.js +++ b/apps/web/src/lliga/nomsLliga.js @@ -22,10 +22,10 @@ const CULTURES = { 'Vilaró', 'Bertran', 'Canals', 'Galí', 'Oliveras', 'Rierola'] }, val: { - f: ['Amparo', 'Pepa', 'Vicenta', 'Empar', 'Rosa', 'Neus', 'Llum', 'Carme', 'Lourdes', 'Inma', 'Xelo', 'Maria'], - m: ['Vicent', 'Ximo', 'Pepe', 'Toni', 'Xavi', 'Batiste', 'Joanjo', 'Quique', 'Rafa', 'Pau', 'Nelo', 'Paco'], - s: ['Ferrer', 'Peris', 'Climent', 'Llopis', 'Gisbert', 'Martí', 'Bataller', 'Soriano', 'Escrivà', 'Sanchis', - 'Alemany', 'Monzó', 'Ballester', 'Mompó', 'Tormo', 'Boix', 'Cervera', 'Calatayud', 'Sendra', 'Gil'] + f: ['Fina', 'Pepa', 'Vicenta', 'Empar', 'Rosa', 'Neus', 'Llum', 'Carme', 'Remei', 'Imma', 'Xelo', 'Maria'], + m: ['Vicent', 'Ximo', 'Pasqual', 'Toni', 'Xavi', 'Batiste', 'Joanjo', 'Quico', 'Rafel', 'Pau', 'Nelo', 'Cento'], + s: ['Ferrer', 'Peris', 'Climent', 'Llopis', 'Gisbert', 'Martí', 'Bataller', 'Fuster', 'Escrivà', 'Sanchis', + 'Alemany', 'Monzó', 'Ballester', 'Mompó', 'Tormo', 'Boix', 'Cervera', 'Ribes', 'Sendra', 'Gil'] }, bal: { f: ['Aina', 'Joana', 'Margalida', 'Catalina', 'Francina', 'Antònia', 'Maria', 'Bel', 'Xisca', 'Apol·lònia'], @@ -46,10 +46,10 @@ const CULTURES = { 'Ribas', 'Babot', 'Naudi', 'Riba'] }, franja: { - f: ['Pilar', 'Carmen', 'Rosa', 'Teresa', 'Núria', 'Montse', 'Ana', 'Mercè'], + f: ['Pilar', 'Carme', 'Rosa', 'Teresa', 'Núria', 'Montse', 'Anna', 'Mercè'], m: ['Josep', 'Ramon', 'Joaquim', 'Manel', 'Jesús', 'Àngel', 'Xavier', 'Hèctor'], - s: ['Lacasa', 'Salvador', 'Pallarés', 'Arnau', 'Sancho', 'Bielsa', 'Castell', 'Monclús', 'Gràcia', - 'Llovet', 'Blasco', 'Ballarín', 'Moliné', 'Abella'] + s: ['Ricart', 'Salvador', 'Pallarès', 'Arnau', 'Farré', 'Fontanet', 'Castell', 'Monclús', 'Gràcia', + 'Llovet', 'Calvet', 'Esteve', 'Moliné', 'Abella'] }, alguer: { f: ['Maria', 'Antonella', 'Giovanna', 'Caterina', 'Rita', 'Paola', 'Margalida'], @@ -57,6 +57,9 @@ const CULTURES = { s: ['Sechi', 'Piras', 'Mura', 'Carta', 'Pinna', 'Delogu', 'Scanu', 'Manca', 'Pais', 'Fois', 'Serra', 'Bardino'] }, // ── Gent d'altres orígens que viu als territoris ── + // (els noms castellans i de l'Amèrica hispana ja no surten a les lligues + // dels Països Catalans: només a Europa i al món, i per a qui juga des + // de fora; vegeu DIVERSE) es: { f: ['Laura', 'Lucía', 'Cristina', 'Sara', 'Raquel', 'Elena', 'Paula', 'Silvia', 'Beatriz', 'Irene'], m: ['Sergio', 'Javier', 'Alejandro', 'David', 'Álvaro', 'Rubén', 'Iván', 'Pablo', 'Daniel', 'Adrián'], @@ -390,17 +393,30 @@ const CULTURES = { }; // Cultura de cada territori dels Països Catalans, i de quins altres -// orígens és la gent que hi viu (pesos). +// orígens és la gent que hi viu, en percentatge de la proporció de cada +// nivell (DIVERSITY_BY_LEVEL). El que falta fins a 100 són noms del +// territori: abans hi havia noms castellans i de l'Amèrica hispana, i ara +// a les lligues dels Països Catalans tots els noms i cognoms són catalans +// o d'altres llengües. const TERRITORY_CULTURE = { cat: 'cat', val: 'val', bal: 'bal', and: 'and', nord: 'nord', franja: 'franja', alguer: 'alguer' }; const DIVERSE = { - cat: [['es', 55], ['latam', 15], ['ma', 12], ['ro', 8], ['cn', 5], ['pk', 5]], - val: [['es', 65], ['latam', 10], ['ro', 15], ['ma', 10]], - bal: [['es', 55], ['de', 15], ['latam', 15], ['ma', 10], ['it', 5]], - and: [['es', 45], ['pt', 45], ['fr', 10]], + cat: [['ma', 12], ['ro', 8], ['cn', 5], ['pk', 5]], + val: [['ro', 15], ['ma', 10]], + bal: [['de', 15], ['ma', 10], ['it', 5]], + and: [['pt', 45], ['fr', 10]], nord: [['fr', 85], ['ma', 15]], - franja: [['es', 85], ['ro', 15]], + franja: [['ro', 15]], alguer: [['it', 100]] }; +// Cultures del castellà: no surten mai a les lligues dels Països Catalans. +const CASTILIAN_CULTURES = ['es', 'latam']; +// Formes castellanes que hi havia entre els noms dels territoris, i la +// catalana que les substitueix (per als rivals de temporades ja començades). +const CATALAN_FORMS = { + 'Amparo': 'Fina', 'Lourdes': 'Remei', 'Inma': 'Imma', 'Pepe': 'Pasqual', 'Quique': 'Quico', 'Paco': 'Cento', + 'Rafa': 'Rafel', 'Soriano': 'Fuster', 'Calatayud': 'Ribes', 'Carmen': 'Carme', 'Ana': 'Anna', 'Lacasa': 'Ricart', + 'Pallarés': 'Pallarès', 'Sancho': 'Farré', 'Bielsa': 'Fontanet', 'Blasco': 'Calvet', 'Ballarín': 'Esteve' +}; // Proporció de rivals d'altres orígens als nivells de casa (municipal, // comarcal, provincial, territori) i als Països Catalans. const DIVERSITY_BY_LEVEL = [0, 0.12, 0.25, 0.4, 0.2]; @@ -464,29 +480,90 @@ function makeName(code, random) { // Cultura del nom d'un rival segons el nivell (0 municipal ... 6 mundial), // la localitat del jugador i l'origen del rival. options.territoryOf: ciutat // dels Països Catalans → territori (localitats.js). +// Territori d'on surt el nom d'un rival dels nivells de casa i dels Països +// Catalans (0-4): el del jugador o, als Països Catalans i a la Lliga +// Catalana de qui juga a la Catalunya Nord (rivals del Principat i del +// Nord), el d'on és el rival. +function territoryFor(level, home, origin, opts) { + let territory = (home && home.territory) || 'cat'; + if (level === 4 || (level === 3 && territory === 'nord')) { + const t = opts && typeof opts.territoryOf === 'function' ? opts.territoryOf(origin) : null; + territory = t || territory; + } + return territory; +} + function cultureFor(level, home, origin, options) { const opts = options || {}; const rnd = typeof opts.random === 'function' ? opts.random : Math.random; const h = home || {}; if (level >= 5) return countryCulture(origin) || pick(EUROPE_CULTURES, rnd); - let territory = h.territory || 'cat'; - // Als Països Catalans, i a la Lliga Catalana de qui juga a la Catalunya - // Nord (rivals del Principat i del Nord), el nom va amb el territori - // d'on és el rival. - if (level === 4 || (level === 3 && territory === 'nord')) { - const t = typeof opts.territoryOf === 'function' ? opts.territoryOf(origin) : null; - territory = t || territory; - } + const territory = territoryFor(level, h, origin, opts); if (territory === 'fora') return countryCulture(h.pais) || pick(EUROPE_CULTURES, rnd); // A l'Alguer, de la Nurra a Sardenya la majoria de rivals no són // algueresos: noms italians (cada nivell, més). if (territory === 'alguer' && level >= 1 && level <= 3 && rnd() < ALGUER_ITALIAN_BY_LEVEL[level]) return 'it'; const base = TERRITORY_CULTURE[territory] || 'cat'; const share = DIVERSITY_BY_LEVEL[Math.max(0, Math.min(4, level))] || 0; - if (share > 0 && rnd() < share && DIVERSE[territory]) return pickWeighted(DIVERSE[territory], rnd); + const others = DIVERSE[territory]; + if (share > 0 && others && rnd() < share) { + const percent = others.reduce(function (sum, p) { return sum + p[1]; }, 0); + if (rnd() * 100 < percent) return pickWeighted(others, rnd); + } return base; } +// Noms de pila i cognoms del castellà (cultures es i latam). +const CASTILIAN_GIVEN = new Set(), CASTILIAN_SURNAMES = new Set(); +CASTILIAN_CULTURES.forEach(function (code) { + const c = CULTURES[code]; + c.f.concat(c.m).forEach(function (n) { CASTILIAN_GIVEN.add(n); }); + (c.s || []).forEach(function (n) { CASTILIAN_SURNAMES.add(n); }); +}); + +// Un nom fet amb un nom i un cognom castellans (García, Martínez...). +function isCastilianName(name) { + const parts = String(name == null ? '' : name).trim().split(/\s+/); + return parts.length >= 2 && CASTILIAN_GIVEN.has(parts[0]) && CASTILIAN_SURNAMES.has(parts.slice(1).join(' ')); +} + +// El nom d'un rival d'una temporada ja començada, sense res de castellà a +// les lligues dels Països Catalans (nivells 0-4): un nom castellà en dona +// un de nou del territori (cap dels de options.taken, que s'hi afegeix), i +// les formes castellanes d'abans passen a la catalana (Carmen, Carme). A +// Europa, al món i per a qui juga des de fora, el nom no es toca. +function catalanRivalName(name, level, home, origin, options) { + const opts = options || {}; + if (typeof name !== 'string' || !(level >= 0 && level <= 4)) return name; + const territory = territoryFor(level, home || {}, origin, opts); + if (territory === 'fora') return name; + if (!isCastilianName(name)) { + return name.split(' ').map(function (w) { return CATALAN_FORMS[w] || w; }).join(' '); + } + const rnd = typeof opts.random === 'function' ? opts.random : Math.random; + const code = TERRITORY_CULTURE[territory] || 'cat'; + const taken = opts.taken instanceof Set ? opts.taken : new Set(); + // Com a rivalNames: un nom que no hi sigui i, si es pot, sense repetir + // el nom de pila ni el cognom de cap altre rival de la lliga. + const usedGiven = new Set(), usedSurnames = new Set(); + taken.forEach(function (key) { + const parts = String(key).split(' '); + usedGiven.add(parts[0]); + usedSurnames.add(parts.slice(1).join(' ')); + }); + let best = null, bestScore = -1; + for (let k = 0; k < 80 && bestScore < 2; k++) { + const cand = makeName(code, rnd); + const key = fold(cand.name); + if (taken.has(key) || FAMOUS.has(key)) continue; + const score = (usedSurnames.has(fold(cand.surname)) ? 0 : 1) + (usedGiven.has(fold(cand.given)) ? 0 : 1); + if (score > bestScore) { best = cand; bestScore = score; } + } + const got = best ? best.name : makeName(code, rnd).name; + taken.add(fold(got)); + return got; +} + // Noms dels rivals d'una lliga nova: count noms diferents (i, si es pot, // sense repetir nom de pila ni cognom), cap dels d'options.exclude (els de // les darreres temporades). origins: d'on és cada rival (localitats.js: @@ -531,6 +608,8 @@ export { CULTURES, COUNTRY_CULTURE, DIVERSE, + CASTILIAN_CULTURES, + CATALAN_FORMS, DIVERSITY_BY_LEVEL, TERRITORY_CULTURE, FAMOUS, @@ -538,5 +617,7 @@ export { countryCulture, makeName, cultureFor, - rivalNames + rivalNames, + isCastilianName, + catalanRivalName }; diff --git a/docs/LLIGA.md b/docs/LLIGA.md index bf1d9f6..d997283 100644 --- a/docs/LLIGA.md +++ b/docs/LLIGA.md @@ -185,7 +185,19 @@ una temporada començada conserva les jornades i el calendari amb què es va cre de pobles de la comarca a la comarcal… de països a l'europea i la mundial). Els dos fitxers són **còpies** convertides a mòdul ES (`apps/web/src/lliga/nomsLliga.js` i `localitats.js`, amb els seus `.d.ts`). - Si canvien a El Tauler, s'han de tornar a copiar. + Si canvien a El Tauler, s'han de tornar a copiar (es pot fer amb un script: + el cos de la funció d'El Tauler, desindentat, i `export { … }` amb el que + retorna; la conversió dona exactament el fitxer d'aquí). +- **Cap nom castellà a les lligues dels Països Catalans** (del municipi als + Països Catalans, des de qualsevol territori): la gent d'altres orígens de + cada territori ja no inclou noms castellans ni de l'Amèrica hispana (la + seva part són noms del territori), i els noms de cada territori no tenen + formes castellanes (Amparo, Paco, Carmen, Pallarés… → Fina, Cento, Carme, + Pallarès). A la Catalana, des de Vic, un 88 % de noms catalans (abans, 60 %); + la resta, marroquins, romanesos, xinesos i pakistanesos. Les temporades + començades abans els canvien en carregar-les (`catalanRivals`, amb la llavor + de la temporada: sempre els mateixos). A Europa, al món i per a qui juga des + de fora dels Països Catalans, els noms són del país, castellans inclosos. - No es repeteixen els noms de les darreres temporades (`recentNames`, 90). - **Avatar**: les inicials sobre un dels degradats del planter, triat pel nom (sempre el mateix per al mateix rival).