Jump to content

User:Habst/bracket.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
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})`);
}