User:Habst/bracket.js
GENDER = 'W';
EVT = '3000-metres-steeplechase';
graphQlUrl = "https://graphql-prod-4625.prod.aws.worldathletics.org/graphql";
headers = { "x-api-key": "da2-fcprvsdozzce5dx2baifenjwpu" }; // intentionally public
getAthletes = async (disciplineCode, sexCode, round = 'heats', useHeats = true) => {
if (useHeats) {
const url = `https://worldathletics.org/competitions/olympic-games/paris24/results/${sexCode === 'M' ? 'men' : 'women'}/${disciplineCode}/${round}/result`;
console.log(url);
const html = await (await fetch(url)).text();
const nextData = JSON.parse(new DOMParser().parseFromString(html, 'text/html').querySelector('script[id=__NEXT_DATA__]').innerText);
const searchAthletes = nextData.props.pageProps.eventPhasesByDiscipline.units.flatMap(u => u.startlist).map(c => ({
...c, id: c.competitorId_WA,
firstName: c.competitorFirstName, lastName: c.competitorLastName, countryCode: c.competitorCountryCode,
disciplines: [{ nameUrlSlug: disciplineCode }],
}));
return { data: { searchAthletes } };
}
return await (await fetch(graphQlUrl, {
headers,
body: JSON.stringify({
operationName: "SearchAthletes",
variables: { eventId: 7087, disciplineCode, sexCode },
query: `query SearchAthletes($eventId: Int!, $countryCode: String, $disciplineCode: String, $sexCode: String, $searchValue: String) {
searchAthletes(eventId: $eventId, countryCode: $countryCode, disciplineCode: $disciplineCode, sexCode: $sexCode, searchValue: $searchValue) {
id firstName lastName countryCode
disciplines { nameUrlSlug }
__typename
}
}`
}),
method: "POST",
})).json();
}
getCompetitor = async (id) => {
return await (await fetch(graphQlUrl, {
headers,
body: JSON.stringify({
operationName: "GetCompetitorBasicInfo",
variables: { id },
query: `query GetCompetitorBasicInfo($id: Int, $urlSlug: String) {
competitor: getSingleCompetitor(id: $id, urlSlug: $urlSlug) {
worldRankings {
current {
rankingScore place urlSlug
}
}
__typename
}
}`
}),
method: "POST",
})).json();
}
headToHead = async (id, headToHeadOpponent, headToHeadDiscipline) => {
return await (await fetch(graphQlUrl, {
headers,
body: JSON.stringify({
operationName: "headToHead",
variables: { headToHeadDiscipline, headToHeadOpponent, id },
query: `query headToHead($id: Int, $headToHeadDiscipline: String, $headToHeadOpponent: Int, $headToHeadStartDate: String, $headToHeadEndDate: String, $headToHeadFinalOnly: Boolean) {
headToHead(id: $id, headToHeadDiscipline: $headToHeadDiscipline, headToHeadOpponent: $headToHeadOpponent, headToHeadStartDate: $headToHeadStartDate, headToHeadEndDate: $headToHeadEndDate, headToHeadFinalOnly: $headToHeadFinalOnly) {
disciplines { id name }
results {
athlete1Wins athlete2Wins
results {
athlete1Wins athlete2Wins competition date
place1 place2 race result1 result2
__typename
}
__typename
}
__typename
}
}`
}),
method: "POST",
})).json();
}
seed = (num) => {
const nextLayer = (pls) => {
const out = [];
const length = pls.length * 2 + 1;
pls.forEach((d) => {
out.push(d);
out.push(length - d);
});
return out;
}
const rounds = Math.log(num) / Math.log(2) - 1;
let pls = [1, 2];
for (let i = 0; i < rounds; i++) pls = nextLayer(pls);
return pls;
}
isWinner = (a1, a1w, a2, a2w) => {
if (a1w > a2w) return true;
if (a2w > a1w) return false;
if (a1.rank.rankingScore > a2.rank.rankingScore) return true;
return false;
}
n2slug = {
'100-metres': '100m',
'100-metres-hurdles': '100mh',
'110-metres-hurdles': '110mh',
'200-metres': '200m',
'400-metres': '400m',
'400-metres-hurdles': '400mh',
'800-metres': '800m',
'1500-metres': '1500m',
'3000-metres-steeplechase': '3000msc',
'5000-metres': '5000m',
'10000-metres': '10000m',
'20-kilometres-race-walk': ['20km-race-walking', 'race-walking'],
'triple-jump': 'triple-jump',
};
n2h2h = {
'100-metres': ['e10229630'],
'1500-metres': ['e10229502'],
'10000-metres': ['e10229610'],
'20-kilometres-race-walk': ['e10229508', 'e10229535'],
};
window.h2h ??= {};
doMatch = async (ath1, ath2) => {
let h2hDisc = n2h2h[ath1.disciplines[0].nameUrlSlug];
if (Array.isArray(h2hDisc)) h2hDisc = h2hDisc[GENDER === 'M' ? 0 : 1];
const key = `${[ath1.id, ath2.id].toSorted().join('_')}_${h2hDisc}`;
h2h[key] ??= await headToHead(ath1.id, ath2.id, h2hDisc);
if (!h2hDisc) {
const discs = h2h[key].data.headToHead.disciplines;
h2hDisc = discs.find(d => d.name.toLowerCase() === EVT.toLowerCase().replaceAll('-', ' '))?.id;
if (!h2hDisc) { console.log(JSON.stringify(discs)); return {}; }
h2h[key] = await headToHead(ath1.id, ath2.id, h2hDisc);
}
const results = h2h[key].data.headToHead.results;
const [winner, loser] = isWinner(ath1, results.athlete1Wins, ath2, results.athlete2Wins) ? [ath1, ath2] : [ath2, ath1];
const [winnerWins, loserWins] = [results.athlete1Wins, results.athlete2Wins].sort((a, b) => a - b).reverse();
return { winner, loser, winnerWins, loserWins };
}
window.searchAthletes ??= await getAthletes(EVT, GENDER, 'final');
window.cs ??= {};
for (const ath of window.searchAthletes.data.searchAthletes) {
cs[ath.id] ??= await getCompetitor(ath.id);
const nameUrlSlug = ath.disciplines[0].nameUrlSlug;
let urlSlug = n2slug[nameUrlSlug] ?? nameUrlSlug;
if (Array.isArray(urlSlug)) urlSlug = urlSlug[GENDER === 'M' ? 0 : 1];
const rank = cs[ath.id].data.competitor.worldRankings?.current.find(wr => urlSlug === wr.urlSlug);
if (!rank) console.log(cs[ath.id].data.competitor.worldRankings?.current);
ath.rank = rank ?? { rankingScore: 0 };
}
athletes = window.searchAthletes.data.searchAthletes.sort((a, b) => b.rank.rankingScore - a.rank.rankingScore);
athletes = athletes.slice(0, athletes.length >= 16 ? 16 : athletes.length >= 8 ? 8 : athletes.length >= 4 ? 4 : 2);
window.genBracket ??= (await import('https://unpkg.com/ascii-tournament-bracket')).default;
matches = seed(athletes.length).map(n => athletes[n - 1]);
bracket = ...matches].map((a, i, arr) => i % 2 ? null : [arr[i], arr[i + 1.map(c => `${c.lastName} #${c.rank.place}`)).filter(x => x)];
semiLosers = [];
stop = false;
while (matches.length > 1) {
bracket.push([]);
console.log(matches);
winners = [];
for (let i = 0; i < matches.length; i += 2) {
const ath1 = matches[i];
const ath2 = matches[i + 1];
const { winner, winnerWins, loser, loserWins } = await doMatch(ath1, ath2);
if (!winner) { stop = true; break; }
console.log(winner.lastName, winnerWins, 'over', loser.lastName, loserWins);
winners.push(winner);
if (bracket.at(-1).length === 0) bracket.at(-1).push([]);
if (bracket.at(-1).at(-1).length >= 2) bracket.at(-1).push([]);
if (matches.length === 4) semiLosers.push(loser);
bracket.at(-1).at(-1).push(`${winner.lastName} over ${loser.lastName} (${winnerWins}-${loserWins})`);
}
if (stop) break;
matches = [...winners];
}
if (!stop) {
bracketLs = genBracket(...bracket.slice(0, -1)).split('\n').filter(s => s.trim());
bracketLs[Math.floor(bracketLs.length / 2)] += ' ' + bracket.at(-1);
console.log(bracketLs.join('\n'));
const { winner, winnerWins, loser, loserWins } = await doMatch(...semiLosers);
console.log(`Bronze medal match: ${winner.lastName} over ${loser.lastName} (${winnerWins}-${loserWins})`);
}