237 lines
11 KiB
JavaScript
237 lines
11 KiB
JavaScript
/**
|
||
* FIFA World Cup 2026 reference data — June 11–July 19, 2026.
|
||
*
|
||
* Static, hand-curated. The poller pulls fixtures/standings/squads from
|
||
* APIs (football-data.org + worldcup2026 OSS); this file holds the
|
||
* intelligence the APIs don't carry: venue altitudes, host-continent
|
||
* teams, designated penalty/set-piece takers, and historical tournament
|
||
* performers.
|
||
*
|
||
* Updated manually as the tournament progresses (squad confirmations,
|
||
* penalty-taker shifts, etc.). Not consumed during normal cache misses —
|
||
* the feature extractor reads this once per process load.
|
||
*
|
||
* Venue altitudes sourced from public elevation data (NOAA / Google
|
||
* Earth, ft above sea level). Estadio Akron is in Zapopan (Guadalajara
|
||
* metro) at ~5,100 ft; Estadio Azteca's stadium floor sits at ~7,349 ft
|
||
* — the highest altitude in tournament history for the host venue.
|
||
*/
|
||
|
||
const VENUES = Object.freeze({
|
||
// United States (11)
|
||
'AT&T Stadium': { city: 'Arlington, TX', altitude_ft: 600, climate: 'hot', country: 'USA' },
|
||
'Mercedes-Benz Stadium': { city: 'Atlanta, GA', altitude_ft: 1050, climate: 'hot_humid', country: 'USA' },
|
||
'Gillette Stadium': { city: 'Foxborough, MA', altitude_ft: 260, climate: 'temperate', country: 'USA' },
|
||
'NRG Stadium': { city: 'Houston, TX', altitude_ft: 80, climate: 'hot_humid', country: 'USA' },
|
||
'Arrowhead Stadium': { city: 'Kansas City, MO', altitude_ft: 820, climate: 'temperate', country: 'USA' },
|
||
'SoFi Stadium': { city: 'Inglewood, CA', altitude_ft: 100, climate: 'temperate', country: 'USA' },
|
||
'Hard Rock Stadium': { city: 'Miami Gardens, FL', altitude_ft: 10, climate: 'hot_humid', country: 'USA' },
|
||
'MetLife Stadium': { city: 'East Rutherford, NJ', altitude_ft: 7, climate: 'temperate', country: 'USA' },
|
||
'Lincoln Financial Field': { city: 'Philadelphia, PA', altitude_ft: 30, climate: 'temperate', country: 'USA' },
|
||
"Levi's Stadium": { city: 'Santa Clara, CA', altitude_ft: 33, climate: 'temperate', country: 'USA' },
|
||
'Lumen Field': { city: 'Seattle, WA', altitude_ft: 15, climate: 'cool', country: 'USA' },
|
||
// Canada (2)
|
||
'BMO Field': { city: 'Toronto, ON', altitude_ft: 250, climate: 'temperate', country: 'Canada' },
|
||
'BC Place': { city: 'Vancouver, BC', altitude_ft: 33, climate: 'cool', country: 'Canada' },
|
||
// Mexico (3)
|
||
'Estadio Azteca': { city: 'Mexico City', altitude_ft: 7349, climate: 'altitude', country: 'Mexico' },
|
||
'Estadio BBVA': { city: 'Monterrey', altitude_ft: 1765, climate: 'hot', country: 'Mexico' },
|
||
'Estadio Akron': { city: 'Guadalajara (Zapopan)', altitude_ft: 5138, climate: 'altitude', country: 'Mexico' },
|
||
});
|
||
|
||
// Host-continent teams (CONCACAF) — historically benefit from reduced
|
||
// travel, fan support, and altitude acclimation. The 2026 hosts (USA,
|
||
// Canada, Mexico) auto-qualified; the rest qualified through CONCACAF.
|
||
const CONCACAF_TEAMS = Object.freeze([
|
||
'USA', 'Canada', 'Mexico', 'Costa Rica', 'Jamaica', 'Honduras',
|
||
'Panama', 'El Salvador', 'Haiti', 'Trinidad and Tobago',
|
||
'Guatemala', 'Curacao', 'Suriname',
|
||
]);
|
||
|
||
// CONMEBOL teams travel less than European/African squads and are
|
||
// historically strong in 2026-style climates. Used as a softer secondary
|
||
// modifier (not full home-continent advantage).
|
||
const CONMEBOL_TEAMS = Object.freeze([
|
||
'Argentina', 'Brazil', 'Uruguay', 'Colombia', 'Ecuador', 'Paraguay',
|
||
'Peru', 'Chile', 'Venezuela', 'Bolivia',
|
||
]);
|
||
|
||
// Designated penalty takers — primary plus secondary fallback if primary
|
||
// is off the pitch. Sourced from each team's most recent qualifier or
|
||
// pre-tournament friendly; updated as confirmations come in.
|
||
//
|
||
// Penalty-taker status adds ~0.15 goals per 90 to the player's base rate.
|
||
// Keys are team names matching football-data.org's `team.name` field;
|
||
// values are arrays in preference order.
|
||
const PENALTY_TAKERS = Object.freeze({
|
||
'Argentina': ['Lionel Messi', 'Lautaro Martínez'],
|
||
'Brazil': ['Vinicius Junior', 'Neymar'],
|
||
'France': ['Kylian Mbappé', 'Antoine Griezmann'],
|
||
'England': ['Harry Kane', 'Bukayo Saka'],
|
||
'Portugal': ['Cristiano Ronaldo', 'Bruno Fernandes'],
|
||
'Spain': ['Álvaro Morata', 'Mikel Merino'],
|
||
'Germany': ['Kai Havertz', 'İlkay Gündoğan'],
|
||
'Netherlands': ['Memphis Depay', 'Cody Gakpo'],
|
||
'Belgium': ['Romelu Lukaku', 'Kevin De Bruyne'],
|
||
'Italy': ['Jorginho', 'Lorenzo Pellegrini'],
|
||
'Croatia': ['Luka Modrić', 'Andrej Kramarić'],
|
||
'Uruguay': ['Darwin Núñez', 'Federico Valverde'],
|
||
'Colombia': ['James Rodríguez', 'Luis Díaz'],
|
||
'Mexico': ['Raúl Jiménez', 'Hirving Lozano'],
|
||
'USA': ['Christian Pulisic', 'Folarin Balogun'],
|
||
'Canada': ['Jonathan David', 'Alphonso Davies'],
|
||
'Morocco': ['Hakim Ziyech', 'Achraf Hakimi'],
|
||
'Senegal': ['Sadio Mané', 'Ismaïla Sarr'],
|
||
'Japan': ['Takefusa Kubo', 'Wataru Endō'],
|
||
'South Korea': ['Son Heung-min', 'Hwang Hee-chan'],
|
||
'Australia': ['Jackson Irvine', 'Mitchell Duke'],
|
||
'Switzerland': ['Granit Xhaka', 'Xherdan Shaqiri'],
|
||
'Poland': ['Robert Lewandowski', 'Piotr Zieliński'],
|
||
'Denmark': ['Christian Eriksen', 'Pierre-Emile Højbjerg'],
|
||
'Serbia': ['Aleksandar Mitrović', 'Dušan Vlahović'],
|
||
});
|
||
|
||
// Designated corner takers (set-piece delivery role). Multi-name arrays
|
||
// reflect rotation; the first is the most common deliverer. Corner-taker
|
||
// status meaningfully boosts assist probability for headed goals.
|
||
const CORNER_TAKERS = Object.freeze({
|
||
'Argentina': ['Lionel Messi', 'Ángel Di María'],
|
||
'Brazil': ['Lucas Paquetá', 'Bruno Guimarães'],
|
||
'France': ['Antoine Griezmann', 'Kylian Mbappé'],
|
||
'England': ['Bukayo Saka', 'Phil Foden', 'Trent Alexander-Arnold'],
|
||
'Portugal': ['Bruno Fernandes', 'João Cancelo'],
|
||
'Spain': ['Dani Olmo', 'Mikel Merino'],
|
||
'Germany': ['Joshua Kimmich', 'Toni Kroos'],
|
||
'Netherlands': ['Frenkie de Jong', 'Cody Gakpo'],
|
||
'Belgium': ['Kevin De Bruyne', 'Yannick Carrasco'],
|
||
'Italy': ['Lorenzo Pellegrini', 'Federico Chiesa'],
|
||
'Croatia': ['Luka Modrić', 'Mateo Kovačić'],
|
||
'Uruguay': ['Federico Valverde', 'Giorgian de Arrascaeta'],
|
||
'Mexico': ['Andrés Guardado', 'Hirving Lozano'],
|
||
'USA': ['Christian Pulisic', 'Gio Reyna'],
|
||
'Canada': ['Stephen Eustáquio', 'Tajon Buchanan'],
|
||
});
|
||
|
||
// Direct free-kick specialists. These players take long-range and
|
||
// dangerous-area free kicks. Boosts both goal AND assist probability
|
||
// when a foul is drawn in shooting range.
|
||
const FREE_KICK_TAKERS = Object.freeze({
|
||
'Argentina': ['Lionel Messi'],
|
||
'Brazil': ['Neymar', 'Vinicius Junior'],
|
||
'France': ['Kylian Mbappé'],
|
||
'England': ['Trent Alexander-Arnold', 'Bukayo Saka'],
|
||
'Portugal': ['Cristiano Ronaldo', 'Bruno Fernandes'],
|
||
'Spain': ['Dani Olmo'],
|
||
'Germany': ['Joshua Kimmich'],
|
||
'Belgium': ['Kevin De Bruyne'],
|
||
'Italy': ['Federico Chiesa'],
|
||
'Croatia': ['Luka Modrić'],
|
||
'Colombia': ['James Rodríguez'],
|
||
'Mexico': ['Raúl Jiménez'],
|
||
'USA': ['Christian Pulisic'],
|
||
'Morocco': ['Hakim Ziyech'],
|
||
'South Korea': ['Son Heung-min'],
|
||
'Poland': ['Piotr Zieliński'],
|
||
'Serbia': ['Dušan Tadić'],
|
||
});
|
||
|
||
// "Tournament players" — historical World Cup performers with three or
|
||
// more career WC goals. These names lift the prior on big-game scoring.
|
||
// Threshold: >=3 career WC goals OR >=2 in the most recent WC.
|
||
const TOURNAMENT_PLAYERS = Object.freeze({
|
||
'Lionel Messi': { wc_goals_career: 13, wc_appearances: 26 },
|
||
'Cristiano Ronaldo': { wc_goals_career: 8, wc_appearances: 22 },
|
||
'Kylian Mbappé': { wc_goals_career: 12, wc_appearances: 14 },
|
||
'Harry Kane': { wc_goals_career: 8, wc_appearances: 11 },
|
||
'Neymar': { wc_goals_career: 8, wc_appearances: 16 },
|
||
'Olivier Giroud': { wc_goals_career: 5, wc_appearances: 18 },
|
||
'Antoine Griezmann': { wc_goals_career: 6, wc_appearances: 17 },
|
||
'Romelu Lukaku': { wc_goals_career: 5, wc_appearances: 11 },
|
||
'Luka Modrić': { wc_goals_career: 2, wc_appearances: 18 }, // captain bias
|
||
'Robert Lewandowski': { wc_goals_career: 2, wc_appearances: 8 },
|
||
'Karim Benzema': { wc_goals_career: 3, wc_appearances: 11 },
|
||
'Edinson Cavani': { wc_goals_career: 4, wc_appearances: 14 },
|
||
'Luis Suárez': { wc_goals_career: 7, wc_appearances: 14 },
|
||
'Andrés Guardado': { wc_goals_career: 1, wc_appearances: 20 }, // captain bias
|
||
'Thomas Müller': { wc_goals_career: 10, wc_appearances: 16 },
|
||
'Eden Hazard': { wc_goals_career: 3, wc_appearances: 11 },
|
||
'Hirving Lozano': { wc_goals_career: 2, wc_appearances: 7 },
|
||
'Sadio Mané': { wc_goals_career: 1, wc_appearances: 5 },
|
||
});
|
||
|
||
// Lookup helpers — case-insensitive on player names, exact-match on
|
||
// team names. Each returns a primitive so the feature extractor can
|
||
// drop the result straight into the feature vector.
|
||
|
||
function isPenaltyTaker(playerName, teamName) {
|
||
if (!playerName || !teamName) return false;
|
||
const takers = PENALTY_TAKERS[teamName];
|
||
if (!Array.isArray(takers)) return false;
|
||
const p = String(playerName).toLowerCase();
|
||
return takers.some((t) => t.toLowerCase() === p);
|
||
}
|
||
|
||
function isCornerTaker(playerName, teamName) {
|
||
if (!playerName || !teamName) return false;
|
||
const takers = CORNER_TAKERS[teamName];
|
||
if (!Array.isArray(takers)) return false;
|
||
const p = String(playerName).toLowerCase();
|
||
return takers.some((t) => t.toLowerCase() === p);
|
||
}
|
||
|
||
function isFreeKickTaker(playerName, teamName) {
|
||
if (!playerName || !teamName) return false;
|
||
const takers = FREE_KICK_TAKERS[teamName];
|
||
if (!Array.isArray(takers)) return false;
|
||
const p = String(playerName).toLowerCase();
|
||
return takers.some((t) => t.toLowerCase() === p);
|
||
}
|
||
|
||
function getTournamentHistory(playerName) {
|
||
if (!playerName) return null;
|
||
// Exact match first, then case-insensitive scan.
|
||
if (TOURNAMENT_PLAYERS[playerName]) return TOURNAMENT_PLAYERS[playerName];
|
||
const p = String(playerName).toLowerCase();
|
||
for (const [name, history] of Object.entries(TOURNAMENT_PLAYERS)) {
|
||
if (name.toLowerCase() === p) return history;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function isHomeContinent(teamName) {
|
||
if (!teamName) return false;
|
||
return CONCACAF_TEAMS.includes(teamName);
|
||
}
|
||
|
||
function getVenue(venueName) {
|
||
if (!venueName) return null;
|
||
return VENUES[venueName] || null;
|
||
}
|
||
|
||
// Classify altitude impact for non-acclimatized teams. The historical
|
||
// goal-output reduction kicks in around 1,500 ft and gets material above
|
||
// 4,000 ft (per CSIC studies on player physiology at altitude).
|
||
function altitudeImpact(altitudeFt) {
|
||
if (!Number.isFinite(altitudeFt)) return 'none';
|
||
if (altitudeFt >= 4000) return 'high';
|
||
if (altitudeFt >= 1500) return 'moderate';
|
||
return 'none';
|
||
}
|
||
|
||
module.exports = {
|
||
VENUES,
|
||
CONCACAF_TEAMS,
|
||
CONMEBOL_TEAMS,
|
||
PENALTY_TAKERS,
|
||
CORNER_TAKERS,
|
||
FREE_KICK_TAKERS,
|
||
TOURNAMENT_PLAYERS,
|
||
isPenaltyTaker,
|
||
isCornerTaker,
|
||
isFreeKickTaker,
|
||
getTournamentHistory,
|
||
isHomeContinent,
|
||
getVenue,
|
||
altitudeImpact,
|
||
};
|