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})`);

}