Profile picture

GustavBylund

+46 (0)73 026 26 86hello@gustavbylund.semaistho

Calculate your CSGO winrate

This is a bookmarklet that you can use to calculate some interesting statistics using Steams GDPR-available data.

You need to run it at steamcommunity.com

Bookmarklet

Drag the following link to your bookmark bar!

CSGO Stats

Or add this code to a bookmark:

javascript:var CsgoMap;!function(e){e.de_mirage="Mirage",e.de_dust2="Dust II",e.de_subzero="Subzero",e.de_train="Train",e.de_nuke="Nuke",e.de_inferno="Inferno",e.de_cache="Cache",e.de_overpass="Overpass"}(CsgoMap=CsgoMap||{});class MinMaxAvg{min=1/0;max=-1/0;avg=0;total=0;append(e,t){this.min=Math.min(e,this.min),this.max=Math.max(e,this.max),this.total+=e,this.avg=this.total/t}}class PlayerStats{link;image;name;constructor(e){this.link=e.link,this.image=e.image,this.name=e.name}timesPlayed=0;roundsPlayed=0;ping=new MinMaxAvg;kills=new MinMaxAvg;assists=new MinMaxAvg;deaths=new MinMaxAvg;mvps=new MinMaxAvg;headshotRate=new MinMaxAvg;score=new MinMaxAvg;add(e,t){this.roundsPlayed+=t,this.timesPlayed+=1,this.ping.append(e.ping,this.timesPlayed),this.kills.append(e.kills,this.timesPlayed),this.assists.append(e.assists,this.timesPlayed),this.deaths.append(e.deaths,this.timesPlayed),this.mvps.append(e.mvps,this.timesPlayed),this.headshotRate.append(Number(e.headshotRate),this.timesPlayed),this.score.append(e.score,this.timesPlayed)}}const CACHE_KEY="__maistho_csgo_stats_cached_matches";function getCachedMatches(){try{const e=JSON.parse(localStorage.getItem(CACHE_KEY)||"");return e.forEach(e=>e.time=new Date(e.time)),console.log(e),e}catch(e){return localStorage.removeItem(CACHE_KEY),[]}}async function fetchWithRetry(e,a){for(let t=0;t<5;++t)try{return fetch(e,a)}catch(e){console.warn(e),await new Promise(e=>setTimeout(e,500*t))}}let profileLink;async function getAllMatches(){if("steamcommunity.com"!=window.location.hostname)throw window.location.href="https://steamcommunity.com/my/gcpd/730?tab=matchhistorycompetitive",new Error("Redirecting to steamcommunity.com, please run the bookmarklet again after the page has reloaded");const e=document.querySelector("#global_actions>a.user_avatar");if(!e)throw new Error("You need to be logged in");profileLink=e.href.replace(/\/$/,"");const t=getCachedMatches();var a=t.reduce((e,t)=>(e[t.time.toISOString()]=!0,e),{});const n=[];let s,o=0,i=!1;do{const c=new URLSearchParams({ajax:"1",tab:"matchhistorycompetitive"});s&&c.append("continue_token",s);var r=await fetchWithRetry(profileLink+"/gcpd/730?"+c.toString(),{credentials:"include"}).then(e=>e.json());if(!r.success)throw new Error(JSON.stringify(r));const d=new DOMParser,m=d.parseFromString(r.html,"text/html");for(const h of Array.from(m.querySelectorAll(".generic_kv_table.csgo_scoreboard_root>tbody>tr"))){const g={map:getMap(h),time:getTime(h),waitTime:getWaitTime(h),matchDuration:getMatchDuration(h),score:getScore(h),demo:getDemo(h),...getTeams(h,profileLink)};if(g.map){if(a[g.time.toISOString()]){i=!0;break}n.push(g)}}s=r.continue_token,loadingStatus.innerText=++o+" pages parsed..."}while(!i&&s);var l=[...n,...t];return localStorage.setItem(CACHE_KEY,JSON.stringify(l)),l}const getTeams=(e,t)=>{const a=[[],[]];let n=-1;for(const s of Array.from(e.querySelectorAll(".csgo_scoreboard_inner_right>tbody>tr"))){const o=q(s,"a.linkTitle");null==o?n+=1:a[n].push({link:o.attributes.getNamedItem("href").textContent||"",name:getInfo(s,"a.linkTitle"),image:getAttribute(q(s,".playerAvatar img"),"src"),ping:parseInt(getInfo(s,"td:nth-child(2)")),kills:parseInt(getInfo(s,"td:nth-child(3)")),assists:parseInt(getInfo(s,"td:nth-child(4)")),deaths:parseInt(getInfo(s,"td:nth-child(5)")),mvps:parseInt(getInfo(s,"td:nth-child(6)").substr(1))||0,headshotRate:Number(getInfo(s,"td:nth-child(7)").replace(/%$/,""))/100,score:parseInt(getInfo(s,"td:nth-child(8)"))})}e=a.findIndex(e=>e.some(e=>e.link===t));return{teams:a,userTeam:e}},getDemo=e=>{e=q(e,".csgo_scoreboard_inner_left .csgo_scoreboard_btn_gotv");return getAttribute(e&&e.parentElement,"href")},getScore=e=>getInfo(e,".csgo_scoreboard_score"),getMatchDuration=e=>getInfo(e,".csgo_scoreboard_inner_left>tbody>tr:nth-child(4)").replace(/^Match Duration: /,""),getWaitTime=e=>getInfo(e,".csgo_scoreboard_inner_left>tbody>tr:nth-child(3)").replace(/^Wait Time: /,""),getTime=e=>new Date(getInfo(e,".csgo_scoreboard_inner_left>tbody>tr:nth-child(2)").replace(/\s*GMT$/i,"")),getMap=e=>getInfo(e,".csgo_scoreboard_inner_left>tbody>tr:nth-child(1)").replace(/^Competitive /,"");function getInfo(e,t){return getText(q(e,t))}function getText(e){return(e&&e.textContent||"").trim()}function q(e,t){return e&&e.querySelector(t)}function getAttribute(e,t){return(e&&e.getAttribute(t)||"").trim()}function parseMatches(e){const c={},d={};return e.forEach(e=>{var t=e.score.split(":").map(e=>e.trim()).map(e=>Number(e)),a=t[0]+t[1],n=e.userTeam,s=0===n?1:0;c[e.map]||(c[e.map]={name:e.map,timesPlayed:0,wins:0,losses:0,draws:0,winrate:0,roundsWon:0,roundsLost:0,roundWinrate:0});const o=c[e.map];t[n]>t[s]?o.wins+=1:t[n]<t[s]?o.losses+=1:o.draws+=1,o.timesPlayed+=1,o.roundsWon+=t[n],o.roundsLost+=t[s];for(const i of e.teams)for(const r of i){d[r.link]||(d[r.link]=new PlayerStats(r));const l=d[r.link];l.add(r,a)}}),Object.keys(c).forEach(e=>{const t=c[e];t.winrate=t.wins/(t.timesPlayed-t.draws),t.roundWinrate=t.roundsWon/(t.roundsWon+t.roundsLost)}),{maps:c,players:d}}function fmt(e,t=2){return Number(e.toFixed(t))}function displayPlayer(e){return{image:`<a href="${e.link}"><img src="${e.image}"></a>`,name:`<a href="${e.link}">${e.name}</a>`,gamesPlayed:e.timesPlayed,kdr:fmt(e.kills.total/e.deaths.total),totalKills:e.kills.total,killsPerGame:fmt(e.kills.avg),killsPerRound:fmt(e.kills.total/e.roundsPlayed),totalAssists:e.assists.total,assistsPerGame:fmt(e.assists.avg),assistsPerRound:fmt(e.assists.total/e.roundsPlayed),totalDeaths:e.deaths.total,deathsPerGame:fmt(e.deaths.avg),deathsPerRound:fmt(e.deaths.total/e.roundsPlayed),headshots:fmt(e.headshotRate.avg),compareScore:`<button onclick="window.__maistho_csgo_stats_compare('${e.link}');">Compare</button>`}}function printStatistics(e,t,a){const n=t.sort((e,t)=>t.timesPlayed-e.timesPlayed).slice(0,20);var t=Object.values(e).sort((e,t)=>t.winrate-e.winrate).map(e=>({...e,winrate:formatWinrate(e.winrate),roundWinrate:formatWinrate(e.roundWinrate)})),e=(console.table(n),console.table(t),"text-align: center; margin: 2em 0 0.5em 0;"),s="overflow: auto;",t=`
  <section style="${s}">
  <h2 style="${e}">Map statistics</h2>
  ${makeTable(t)}
  </section>
  <section style="${s}">
  <h2 style="${e}">Player statistics</h2>
  ${makeTable(n.map(displayPlayer))}
  </section>
  <section>
    <h2>Most recent game</h2>
    <p>${a[0].time}</p>
    <h2>Earliest game</h2>
    <p>${a[a.length-1].time}</p>
  </section>
  `,s="__maistho_csgo_stats_table_wrapper";let o=document.getElementById(s);o||((o=document.createElement("div")).id=s,o.style.background="#171a21",o.style.color="white",document.body.prepend(o)),o.innerHTML=t;const i=document.querySelector(".responsive_header");i&&(e=o.clientHeight+"px",i.style.marginTop=e)}function makeTable(e){const n=Object.keys(e[0]),s="padding: 0.125rem 0.5rem;";return`<table style="margin: 0 auto; border-collapse: collapse;">
  <thead>
    <tr>
      ${n.map(e=>{e=(""+e[0].toLocaleUpperCase()+e.substr(1)).replace(/([A-Z])/g," $1");return`<th style="${s}">${e}</th>`}).join("\n")}
    </tr>
  </thead>
  <tbody>
    ${e.map((t,a)=>{return`<tr>${n.map(e=>`<td style="${s} ${a%2==0?"background-color: rgba(255,255,255,0.2);":""}">${t[e]}</td>`).join("\n")}</tr>`}).join("\n")}
  </tbody>
  </table>`}function formatWinrate(e){return(100*e).toLocaleString(void 0,{maximumFractionDigits:2,minimumFractionDigits:0})+"%"}const loadingSpinner=document.createElement("div"),loadingStatus=(loadingSpinner.innerHTML='<img src="https://steamcommunity-a.akamaihd.net/public/images/login/throbber.gif">',loadingSpinner.style.display="flex",loadingSpinner.style.alignItems="center",loadingSpinner.style.justifyContent="center",loadingSpinner.style.position="fixed",loadingSpinner.style.top="0",loadingSpinner.style.left="0",loadingSpinner.style.bottom="0",loadingSpinner.style.right="0",loadingSpinner.style.background="rgba(0,0,0,0.5)",loadingSpinner.style.flexDirection="column",loadingSpinner.style.zIndex="999999",document.createElement("div"));loadingStatus.innerText="0 pages parsed...",loadingStatus.style.color="white",loadingStatus.style.margin="1em",loadingSpinner.appendChild(loadingStatus),document.body.appendChild(loadingSpinner);let rawMatches,parsedData;function showModal(e){var t="__maistho_csgo_stats_modal_wrapper";let a=document.getElementById(t);a||((a=document.createElement("div")).id=t,a.style.background="rgba(0,0,0,0.5)",a.style.position="fixed",a.style.top="0",a.style.bottom="0",a.style.left="0",a.style.right="0",a.style.justifyContent="center",a.style.alignItems="center",a.style.zIndex="99999",document.body.append(a)),a.innerHTML="";const n=document.createElement("div");n.style.color="white",n.style.background="#171a21",n.style.padding="1rem",n.innerHTML=e,a.appendChild(n),a.style.display="flex",a.onclick=()=>{a.removeChild(n),a.style.display="none"}}window.__maistho_csgo_stats_compare=i=>{const r={selfScore:0,otherScore:0,selfKills:0,otherKills:0,selfKdr:0,otherKdr:0};rawMatches.filter(e=>e.teams.some(e=>e.some(e=>e.link===i))).forEach(e=>{let t,a;for(const s of e.teams)for(const o of s)if(o.link===i&&(a=o),(t=o.link===profileLink?o:t)&&a)break;var n;t.score>a.score?r.selfScore++:t.score<a.score&&r.otherScore++,t.kills>a.kills?r.selfKills++:t.kills<a.kills&&r.otherKills++,t.deaths&&a.deaths&&(e=t.kills/t.deaths,(n=a.kills/a.deaths)<e?r.selfKdr++:e<n&&r.otherKdr++)}),showModal(`
    <p>You get a higher score ${fmt(r.selfScore/(r.selfScore+r.otherScore)*100,0)}% of the time!
    </p>

    <p>
    You get a higher number of kills ${fmt(r.selfKills/(r.selfKills+r.otherKills)*100,0)}% of the time!
    </p>

    <p>
    You get a higher KDR ${fmt(r.selfKdr/(r.selfKdr+r.otherKdr)*100,0)}% of the time!
    </p>
    `),console.log(r)},getAllMatches().then(e=>{rawMatches=e;var{maps:t,players:a}=parsedData=parseMatches(e);printStatistics(t,Object.values(a),e)}).catch(e=>{console.warn(e),alert(e)}).finally(()=>{document.body.removeChild(loadingSpinner)});

Changelog

  • July 23rd: Added assists and deaths to player table, improved caching. Fixed a bug with Firefox

How it works

I’ve written a different tool earlier which can dump all of your match data to JSON, available at npm/csgo-player-stats

I didn’t like how much of a hassle it was to extract the cookie and thought “Hey, this could be a bookmarklet!”

Everything can be a bookmarklet it you waste invest enough hours on it

Since the original was written in Typescript, so is this bookmarklet. It adds some nifty features, such as comparing stats with your friends.

I’ve also added some features to this blog in order to be able to develop bookmarklet in typescript and transpile/minify it easier. I’ll try to get a blog post on that up later.

I’m pulling the data from Steams GDPR-takeout page: https://steamcommunity.com/my/gcpd/730?tab=matchhistorycompetitive where you can see all competitive matches (?) from the end of 2017 and forwards.

Full code available below:

enum CsgoMap {
  de_mirage = 'Mirage',
  de_dust2 = 'Dust II',
  de_subzero = 'Subzero',
  de_train = 'Train',
  de_nuke = 'Nuke',
  de_inferno = 'Inferno',
  de_cache = 'Cache',
  de_overpass = 'Overpass',
}

interface Match {
  map: CsgoMap
  time: Date
  waitTime: string
  matchDuration: string
  score: string
  demo?: string
  teams: Team[]
  userTeam: number
}

type Team = Player[]

interface Player {
  link: string
  image: string
  name: string
  ping: number
  kills: number
  assists: number
  deaths: number
  mvps: number
  headshotRate: number
  score: number
}

class MinMaxAvg {
  min: number = Infinity
  max: number = -Infinity
  avg: number = 0
  total: number = 0

  append(n: number, totalTimes: number) {
    this.min = Math.min(n, this.min)
    this.max = Math.max(n, this.max)

    this.total += n
    this.avg = this.total / totalTimes
  }
}

class PlayerStats {
  link: string
  image: string
  name: string

  constructor(player: Player) {
    this.link = player.link
    this.image = player.image
    this.name = player.name
  }

  timesPlayed: number = 0
  roundsPlayed: number = 0
  ping = new MinMaxAvg()
  kills = new MinMaxAvg()
  assists = new MinMaxAvg()
  deaths = new MinMaxAvg()
  mvps = new MinMaxAvg()
  headshotRate = new MinMaxAvg()
  score = new MinMaxAvg()

  add(player: Player, rounds: number) {
    this.roundsPlayed += rounds
    this.timesPlayed += 1
    this.ping.append(player.ping, this.timesPlayed)
    this.kills.append(player.kills, this.timesPlayed)
    this.assists.append(player.assists, this.timesPlayed)
    this.deaths.append(player.deaths, this.timesPlayed)
    this.mvps.append(player.mvps, this.timesPlayed)
    this.headshotRate.append(Number(player.headshotRate), this.timesPlayed)
    this.score.append(player.score, this.timesPlayed)
  }
}

const CACHE_KEY = '__maistho_csgo_stats_cached_matches'

function getCachedMatches(): Match[] {
  try {
    const cached = JSON.parse(localStorage.getItem(CACHE_KEY) || '')
    cached.forEach(match => (match.time = new Date(match.time)))
    console.log(cached)
    return cached
  } catch (err) {
    localStorage.removeItem(CACHE_KEY)
    return []
  }
}

async function fetchWithRetry(
  input: RequestInfo,
  init?: RequestInit,
): Promise<Response> {
  for (let i = 0; i < 5; ++i) {
    try {
      return fetch(input, init)
    } catch (err) {
      console.warn(err)
      await new Promise(resolve => setTimeout(resolve, i * 500))
    }
  }
}

let profileLink
async function getAllMatches() {
  if (window.location.hostname != 'steamcommunity.com') {
    window.location.href =
      'https://steamcommunity.com/my/gcpd/730?tab=matchhistorycompetitive'
    throw new Error(
      'Redirecting to steamcommunity.com, please run the bookmarklet again after the page has reloaded',
    )
  }
  const userAvatar: HTMLAnchorElement | null = document.querySelector(
    '#global_actions>a.user_avatar',
  )
  if (!userAvatar) {
    throw new Error('You need to be logged in')
  }

  profileLink = userAvatar.href.replace(/\/$/, '')

  const cachedMatches = getCachedMatches()
  const matchIds = cachedMatches.reduce((ids, match) => {
    ids[match.time.toISOString()] = true
    return ids
  }, {})
  const matches: Match[] = []
  let continueToken: string | undefined
  let i = 0
  let foundCached = false
  do {
    const params = new URLSearchParams({
      ajax: '1',
      tab: 'matchhistorycompetitive',
    })
    if (continueToken) {
      params.append('continue_token', continueToken)
    }
    const url = `${profileLink}/gcpd/730?${params.toString()}`
    const response = await fetchWithRetry(url, {
      credentials: 'include',
    }).then(res => res.json())
    if (!response.success) {
      throw new Error(JSON.stringify(response))
    }

    const parser = new DOMParser()
    const dom = parser.parseFromString(response.html, 'text/html')
    for (const tr of Array.from(
      dom.querySelectorAll('.generic_kv_table.csgo_scoreboard_root>tbody>tr'),
    )) {
      const match = {
        map: getMap(tr),
        time: getTime(tr),
        waitTime: getWaitTime(tr),
        matchDuration: getMatchDuration(tr),
        score: getScore(tr),
        demo: getDemo(tr),
        ...getTeams(tr, profileLink),
      }
      if (match.map) {
        if (matchIds[match.time.toISOString()]) {
          foundCached = true
          break
        }
        matches.push(match)
      }
    }
    continueToken = response.continue_token
    loadingStatus.innerText = `${++i} pages parsed...`
  } while (!foundCached && continueToken)

  const result = [...matches, ...cachedMatches]
  localStorage.setItem(CACHE_KEY, JSON.stringify(result))

  return result
}

const getTeams = (tr: Element, profileLink: string) => {
  const teams: Team[] = [[], []]
  const players = Array.from(
    tr.querySelectorAll('.csgo_scoreboard_inner_right>tbody>tr'),
  )
  let currentTeam = -1
  for (const player of players) {
    const userLink = q(player, 'a.linkTitle')
    if (userLink == null) {
      currentTeam += 1
      continue
    }
    teams[currentTeam].push({
      link: userLink.attributes.getNamedItem('href')!.textContent || '',
      name: getInfo(player, 'a.linkTitle'),
      image: getAttribute(q(player, '.playerAvatar img'), 'src'),
      ping: parseInt(getInfo(player, 'td:nth-child(2)')),
      kills: parseInt(getInfo(player, 'td:nth-child(3)')),
      assists: parseInt(getInfo(player, 'td:nth-child(4)')),
      deaths: parseInt(getInfo(player, 'td:nth-child(5)')),
      mvps: parseInt(getInfo(player, 'td:nth-child(6)').substr(1)) || 0,
      headshotRate:
        Number(getInfo(player, 'td:nth-child(7)').replace(/%$/, '')) / 100,
      score: parseInt(getInfo(player, 'td:nth-child(8)')),
    })
  }
  const userTeam = teams.findIndex(team =>
    team.some(player => player.link === profileLink),
  )
  return { teams, userTeam }
}

const getDemo = (tr: Element | null) => {
  const text = q(tr, '.csgo_scoreboard_inner_left .csgo_scoreboard_btn_gotv')
  const button = text && text.parentElement
  return getAttribute(button, 'href')
}

const getScore = (tr: Element | null) => getInfo(tr, '.csgo_scoreboard_score')

const getMatchDuration = (tr: Element | null) =>
  getInfo(tr, '.csgo_scoreboard_inner_left>tbody>tr:nth-child(4)').replace(
    /^Match Duration: /,
    '',
  )

const getWaitTime = (tr: Element | null) =>
  getInfo(tr, '.csgo_scoreboard_inner_left>tbody>tr:nth-child(3)').replace(
    /^Wait Time: /,
    '',
  )
const getTime = (tr: Element | null) =>
  new Date(
    getInfo(tr, '.csgo_scoreboard_inner_left>tbody>tr:nth-child(2)').replace(
      /\s*GMT$/i,
      '',
    ),
  )

const getMap = (tr: Element | null) =>
  getInfo(tr, '.csgo_scoreboard_inner_left>tbody>tr:nth-child(1)').replace(
    /^Competitive /,
    '',
  ) as CsgoMap

function getInfo(el: Element | null, query: string): string {
  return getText(q(el, query))
}

function getText(el: Element | null): string {
  return ((el && el.textContent) || '').trim()
}

function q(el: Element | null, query: string): Element | null {
  return el && el.querySelector(query)
}

function getAttribute(el: Element | null, attribute: string) {
  return ((el && el.getAttribute(attribute)) || '').trim()
}

type MapData = {
  [key: string]: {
    name: string
    timesPlayed: number
    wins: number
    losses: number
    draws: number
    winrate: number
    roundsWon: number
    roundsLost: number
    roundWinrate: number
  }
}

function parseMatches(data: Match[]) {
  const maps: MapData = {}

  const players: Record<string, PlayerStats> = {}

  data.forEach((match: Match) => {
    const score = match.score
      .split(':')
      .map(s => s.trim())
      .map(s => Number(s))

    const rounds = score[0] + score[1]
    const myTeam = match.userTeam
    const notMyTeam = myTeam === 0 ? 1 : 0
    if (!maps[match.map]) {
      maps[match.map] = {
        name: match.map,
        timesPlayed: 0,
        wins: 0,
        losses: 0,
        draws: 0,
        winrate: 0,
        roundsWon: 0,
        roundsLost: 0,
        roundWinrate: 0,
      }
    }
    const map = maps[match.map]
    if (score[myTeam] > score[notMyTeam]) {
      map.wins += 1
    } else if (score[myTeam] < score[notMyTeam]) {
      map.losses += 1
    } else {
      map.draws += 1
    }
    map.timesPlayed += 1

    map.roundsWon += score[myTeam]
    map.roundsLost += score[notMyTeam]

    for (const team of match.teams) {
      for (const player of team) {
        if (!players[player.link]) {
          players[player.link] = new PlayerStats(player)
        }

        const stats = players[player.link]
        stats.add(player, rounds)
      }
    }
  })

  Object.keys(maps).forEach(name => {
    const map = maps[name]
    map.winrate = map.wins / (map.timesPlayed - map.draws)
    map.roundWinrate = map.roundsWon / (map.roundsWon + map.roundsLost)
  })
  return { maps, players }
}

function fmt(n: number, precision: number = 2) {
  return Number(n.toFixed(precision))
}

function displayPlayer(p: PlayerStats) {
  return {
    image: `<a href="${p.link}"><img src="${p.image}"></a>`,
    name: `<a href="${p.link}">${p.name}</a>`,
    gamesPlayed: p.timesPlayed,
    kdr: fmt(p.kills.total / p.deaths.total),

    totalKills: p.kills.total,
    killsPerGame: fmt(p.kills.avg),
    killsPerRound: fmt(p.kills.total / p.roundsPlayed),

    totalAssists: p.assists.total,
    assistsPerGame: fmt(p.assists.avg),
    assistsPerRound: fmt(p.assists.total / p.roundsPlayed),

    totalDeaths: p.deaths.total,
    deathsPerGame: fmt(p.deaths.avg),
    deathsPerRound: fmt(p.deaths.total / p.roundsPlayed),

    headshots: fmt(p.headshotRate.avg),
    compareScore: `<button onclick="window.__maistho_csgo_stats_compare('${p.link}');">Compare</button>`,
  }
}

function printStatistics(
  maps: MapData,
  players: PlayerStats[],
  matches: Match[],
) {
  const topPlayers = players
    .sort((b, a) => a.timesPlayed - b.timesPlayed)
    .slice(0, 20)
  var data = Object.values(maps)
    .sort((a, b) => b.winrate - a.winrate)
    .map(map => {
      return {
        ...map,
        winrate: formatWinrate(map.winrate),
        roundWinrate: formatWinrate(map.roundWinrate),
      }
    })
  console.table(topPlayers)
  console.table(data)
  const h2Style = `text-align: center; margin: 2em 0 0.5em 0;`
  const sectionStyle = `overflow: auto;`
  let res = `
  <section style="${sectionStyle}">
  <h2 style="${h2Style}">Map statistics</h2>
  ${makeTable(data)}
  </section>
  <section style="${sectionStyle}">
  <h2 style="${h2Style}">Player statistics</h2>
  ${makeTable(topPlayers.map(displayPlayer))}
  </section>
  <section>
    <h2>Most recent game</h2>
    <p>${matches[0].time}</p>
    <h2>Earliest game</h2>
    <p>${matches[matches.length - 1].time}</p>
  </section>
  `

  const ID = '__maistho_csgo_stats_table_wrapper'
  let el = document.getElementById(ID)
  if (!el) {
    el = document.createElement('div')
    el.id = ID
    el.style.background = '#171a21'
    el.style.color = 'white'
    document.body.prepend(el)
  }

  el.innerHTML = res

  const header: HTMLDivElement | null = document.querySelector(
    '.responsive_header',
  )
  if (header) {
    const height = `${el.clientHeight}px`
    header.style.marginTop = height
  }
}

function makeTable(data: any[]) {
  const keys = Object.keys(data[0])
  const tdStyle = `padding: 0.125rem 0.5rem;`
  const headers = keys
    .map(k => {
      const str = `${k[0].toLocaleUpperCase()}${k.substr(1)}`.replace(
        /([A-Z])/g,
        ' $1',
      )
      return `<th style="${tdStyle}">${str}</th>`
    })
    .join('\n')

  const rows = data
    .map((d, i) => {
      const values = keys
        .map(
          k =>
            `<td style="${tdStyle} ${
              i % 2 === 0 ? 'background-color: rgba(255,255,255,0.2);' : ''
            }">${d[k]}</td>`,
        )
        .join('\n')
      return `<tr>${values}</tr>`
    })
    .join('\n')
  return `<table style="margin: 0 auto; border-collapse: collapse;">
  <thead>
    <tr>
      ${headers}
    </tr>
  </thead>
  <tbody>
    ${rows}
  </tbody>
  </table>`
}

function formatWinrate(winrate: number) {
  return (
    (winrate * 100).toLocaleString(undefined, {
      maximumFractionDigits: 2,
      minimumFractionDigits: 0,
    }) + '%'
  )
}

const loadingSpinner = document.createElement('div')
loadingSpinner.innerHTML = `<img src="https://steamcommunity-a.akamaihd.net/public/images/login/throbber.gif">`
loadingSpinner.style.display = 'flex'
loadingSpinner.style.alignItems = 'center'
loadingSpinner.style.justifyContent = 'center'
loadingSpinner.style.position = 'fixed'
loadingSpinner.style.top = '0'
loadingSpinner.style.left = '0'
loadingSpinner.style.bottom = '0'
loadingSpinner.style.right = '0'
loadingSpinner.style.background = 'rgba(0,0,0,0.5)'
loadingSpinner.style.flexDirection = 'column'
loadingSpinner.style.zIndex = '999999'
const loadingStatus = document.createElement('div')
loadingStatus.innerText = '0 pages parsed...'
loadingStatus.style.color = 'white'
loadingStatus.style.margin = '1em'
loadingSpinner.appendChild(loadingStatus)
document.body.appendChild(loadingSpinner)

let rawMatches: Match[]
let parsedData: { maps: MapData; players: Record<string, PlayerStats> }
;(window as any).__maistho_csgo_stats_compare = link => {
  const stats = {
    selfScore: 0,
    otherScore: 0,
    selfKills: 0,
    otherKills: 0,
    selfKdr: 0,
    otherKdr: 0,
  }
  rawMatches
    .filter(match => match.teams.some(t => t.some(p => p.link === link)))
    .forEach(match => {
      let self: Player
      let other: Player
      for (const team of match.teams) {
        for (const p of team) {
          if (p.link === link) {
            other = p
          }
          if (p.link === profileLink) {
            self = p
          }
          if (self && other) {
            break
          }
        }
      }
      if (self.score > other.score) {
        stats.selfScore++
      } else if (self.score < other.score) {
        stats.otherScore++
      }
      if (self.kills > other.kills) {
        stats.selfKills++
      } else if (self.kills < other.kills) {
        stats.otherKills++
      }
      if (self.deaths && other.deaths) {
        const selfKdr = self.kills / self.deaths
        const otherKdr = other.kills / other.deaths
        if (selfKdr > otherKdr) {
          stats.selfKdr++
        } else if (selfKdr < otherKdr) {
          stats.otherKdr++
        }
      }
    })

  showModal(
    `
    <p>You get a higher score ${fmt(
      (stats.selfScore / (stats.selfScore + stats.otherScore)) * 100,
      0,
    )}% of the time!
    </p>

    <p>
    You get a higher number of kills ${fmt(
      (stats.selfKills / (stats.selfKills + stats.otherKills)) * 100,
      0,
    )}% of the time!
    </p>

    <p>
    You get a higher KDR ${fmt(
      (stats.selfKdr / (stats.selfKdr + stats.otherKdr)) * 100,
      0,
    )}% of the time!
    </p>
    `,
  )
  console.log(stats)
}
getAllMatches()
  .then(matches => {
    rawMatches = matches
    parsedData = parseMatches(matches)
    const { maps, players } = parsedData

    printStatistics(maps, Object.values(players), matches)
  })
  .catch(err => {
    console.warn(err)
    alert(err)
  })
  .finally(() => {
    document.body.removeChild(loadingSpinner)
  })

function showModal(content: string) {
  const ID = '__maistho_csgo_stats_modal_wrapper'
  let el = document.getElementById(ID)
  if (!el) {
    el = document.createElement('div')
    el.id = ID
    el.style.background = 'rgba(0,0,0,0.5)'
    el.style.position = 'fixed'
    el.style.top = '0'
    el.style.bottom = '0'
    el.style.left = '0'
    el.style.right = '0'
    el.style.justifyContent = 'center'
    el.style.alignItems = 'center'
    el.style.zIndex = '99999'
    document.body.append(el)
  }
  el.innerHTML = ''
  const modal = document.createElement('div')
  modal.style.color = 'white'
  modal.style.background = '#171a21'
  modal.style.padding = '1rem'
  modal.innerHTML = content
  el.appendChild(modal)
  el.style.display = 'flex'

  el.onclick = () => {
    el.removeChild(modal)
    el.style.display = 'none'
  }
}