Notesclub

BattleBots WC7 ELO

calculator.livemd

BattleBots WC7 ELO

Mix.install([
  {:vega_lite, "~> 0.1.6"},
  {:kino_vega_lite, "~> 0.1.7"},
  {:floki, "~> 0.26.0"},
  {:httpoison, "~> 2.0"}
])

alias VegaLite, as: Vl

Setup

resultsTable = Kino.Input.text("Google Sheets Results URL") |> Kino.render()

botsTable = Kino.Input.text("Google Sheets Bots URL") |> Kino.render()

Kino.nothing()
startingElo = 1500
winString = "WIN"
kFactor = 512

{_botList, botratings} =
  Kino.Input.read(botsTable)
  |> HTTPoison.get!()
  |> Map.get(:body)
  |> Floki.parse_document!()
  |> Floki.find("table tr")
  |> Enum.drop(2)
  |> Enum.map(fn row ->
    row |> Floki.find("td") |> Enum.drop(2) |> Enum.drop(-1) |> Floki.text()
  end)
  |> Enum.map_reduce(%{}, fn bot, botMap -> {bot, Map.put(botMap, bot, startingElo)} end)

{_, matches} =
  Kino.Input.read(resultsTable)
  |> HTTPoison.get!()
  |> Map.get(:body)
  |> Floki.parse_document!()
  |> Floki.find("table tr")
  |> Enum.drop(3)
  |> Enum.chunk_every(2)
  |> Enum.map_reduce(%{1 => [], 2 => [], 3 => [], 4 => []}, fn tallRow, acc ->
    [matches | botAndResults] = tallRow
    [botElement | resultElements] = botAndResults |> Floki.find("td") |> Enum.drop(1)
    botName = Floki.text(botElement)

    results =
      resultElements
      |> Enum.map(fn result -> String.contains?(result |> Floki.text(), winString) end)

    {_, {_, resultsParsed}} =
      matches
      |> Floki.find("td")
      |> Enum.drop(4)
      |> Floki.find("a")
      |> Enum.map_reduce({1, acc}, fn opponent, {i, wonMatches} ->
        return =
          if Enum.at(results, i - 1, false) do
            Map.put(wonMatches, i, [
              %{botName => opponent |> Floki.text()} | Map.get(wonMatches, i)
            ])
          else
            wonMatches
          end

        {opponent, {i + 1, return}}
      end)

    {tallRow, resultsParsed}
  end)

{_, flatMatches} =
  matches
  |> Enum.map_reduce([], fn {_, match}, matches ->
    {match, matches ++ match}
  end)

{_, elo} =
  Enum.map_reduce(flatMatches, botratings, fn match, botratings ->
    [winner | _] = Map.keys(match)
    loser = Map.fetch!(match, winner)
    winnerRating = Map.fetch!(botratings, winner)
    loserRating = Map.fetch!(botratings, loser)
    expectedWinnerScore = 1 / (1 + :math.pow((loserRating - winnerRating) / 400, 10))
    expectedLoserScore = 1 / (1 + :math.pow((winnerRating - loserRating) / 400, 10))

    {match,
     %{
       botratings
       | winner => winnerRating + kFactor * (1 - expectedWinnerScore),
         loser => loserRating + kFactor * (0 - expectedLoserScore)
     }}
  end)

chartableELO =
  Enum.map(elo, fn bot ->
    botName = elem(bot, 0)
    %{"Bot Name" => botName, "ELO" => round(elem(bot, 1))}
  end)

{:ok}
Vl.new()
|> Vl.data_from_values(chartableELO, only: ["Bot Name", "ELO"])
|> Vl.mark(:bar)
|> Vl.encode_field(:x, "Bot Name", type: :nominal)
|> Vl.encode_field(:y, "ELO", type: :quantitative)
|> Vl.encode_field(:color, "ELO", type: :quantitative)
Kino.DataTable.new(chartableELO)