Files
vyndr/src/data/worldcup2026.js
T

237 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* FIFA World Cup 2026 reference data — June 11July 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,
};