Play Smarter, Not Harder --- An Analysis of My 20 Most Recent Dota2 Games

Brace yourself…

After reading the title, you might be perplexed and wondering, “What is DotA2?” Although it is very hard for me to give an “A is B” type of definition to DoTA2 because such kind of definition won’t do it justice, I will do it anyways since I don’t want to intimate you, at the very beginning of my blog, with all that I am about to introduce you to. In short, Dota2 (Defense of the Ancients 2) is a multiplayer online battle arena (MOBA) video game developed and published by Valve. I said Dota2 is a game, but it is not just a game. A match of DotA2 is as competitive as any traditional sports, such as soccer, football, and basketball. Due to how competitive it is really classified as Esports (electronic sports). Moreover, it has well established leagues and seasons like the NBA and the NFL. Clubs around the world compete with each other regularly in Majors and Minors. At the end of each season in August, a world championship called The International is held, in which the 18 strongest teams from around the world play against each other. To give you a sense of the championship’s magnitude, last year, the International 2019 had a prize pool of 34,330,068 US dollars and the wining team (a team has 5 players) was granted a prize of 15,620,181 US dollars. To win a match of DotA2 requires a tremendous amount of skills such as mastery of basic game mechanism, good team coordination, fast and good decision making, patience, calmness, and, believe it or not, physical and mental endurance ,etc. I can say that all avid and royal players of the Dota2, including myself of course, don’t play for fun but compete to win. Even though I probably won’t ever reach the skill level of those professional players, I still strive for improvement every match. But some times when I am blinded by the aspiration for victory, instead of reflecting on my performance after each match, I would just mindlessly keep playing and keep making the same mistake over and over again. Thankfully, The final project of MATH241 Data Science provided me with an incredible opportunity to analyze my game play objectively and rationally using data. But before we begin, I will give you a crash course on the basic mechanism and concepts of Dota2 that are relevent to my analysis.

Here we go…

The Objective

In a match of Dota2, two teams, the Radient team and the Dire team, each of 5 players, play against each other with the goal of destroying the opponent team’s ancient.

Ancients. The left one is the Radient ancient and the right one is the Dire ancient.

Figure 1: Ancients. The left one is the Radient ancient and the right one is the Dire ancient.

At the beginning of the match, the drafting stage, all players take turns to select their heores with unique abilities. As an example, my favorite hero, ember spirit, is shown in the figure below. The icons in the lower right corner represent the unique abilities of ember spirit. After the drafting is finished, the match begins and the two teams start to work their way to the opponent team’s ancient.

Ember spirit, an example of 1 of the 119 heroes in Dota2.

Figure 2: Ember spirit, an example of 1 of the 119 heroes in Dota2.

An example of how a hero look like in game. The hero presented here is ember spirit.

Figure 3: An example of how a hero look like in game. The hero presented here is ember spirit.

But How?

Before one team can reach another team’s ancient, they must at least destory all the towers, which is shown in the figure below, (Marked as T1, T2, T3, T4 on the map) in one of the three lanes, the top lane, the mid lane, and the bottom lane. In most situations, each team will allocate 2 players in the top lane, 1 player in the mid lane, and 2 players in the bottom lane. In order to destroy the towers, the players need to grow stronger than the opponent players that are defending the towers. Players grow stronger by farming gold to purchase items, and gaining experience to strengthen their abilities. Gold and experience can be earned by killing lane creeps (see the third figure below), neutral creeps (see the fourth figure below), enemy heroes, and destorying enemy towers. As you can see in the third figure, lan creeps are divided into two factions, the Radient and the Dire, like the players. Lane creeps also come in groups. In the first 15 minutes of the game, each group of lane creeps consists of 3 melee creeps and 1 range creep. As the game processes, the quantity will increase. However, my analysis will only focus on the first 10 minutes of the game so we default the group size to be 4. The first wave of lane creeps spawns when the game timer counts down to 0. Then, a new wave spawns every 30 second. In each wave, one group of lane creeps will spawn for each lane and go down the lane (direction marked by black arrows in the map) towards the enemy ancient. Players from each team will kill the lane creeps to prevent them from destorying the towers as the creeps move towards the ancient, and to earn gold and experience. When the health (how much damage a creep can take before it dies) of a friendly lane creep is below a certain thresold, players can kill the friendly lane creep to prevent enemy players from getting gold and a portion of the experience. This action is called a deny. Neutral creeps spawn at specific sites in the jungle areas. The first wave of neutral creeps spawns at 1:00 (the 1 minute mark). Moreover, neutral creeps are neutral in the sense that they don’t belong to a specific team. Therefore, the deny machenism does not apply to jungle creeps. In most cases, neutral creeps would not continue to spawn like lane creeps unless they are killed by the players before the full minute mark.

The map of Dota2's battle ground.

Figure 4: The map of Dota2’s battle ground.

The towers. The left one is the Radient tower and the right one is the Dire tower.

Figure 5: The towers. The left one is the Radient tower and the right one is the Dire tower.

The green creeps are Radient creeps and the red creeps are Dire creeps.

Figure 6: The green creeps are Radient creeps and the red creeps are Dire creeps.

Neutral creeps.

Figure 7: Neutral creeps.

As you probably have guessed, one of the ways to build advantage over the enemy team is to kill as many enemy creeps as possible and deny as many friendly creeps as possible. By doing so, players can maximize the gold and experience they earn while minimizing the gold and experience earned by the enemy players. This process is the major focus of the first 10 minutes of the match and since this process is carried out in lane (the top, mid, and bottom lane), the first 10 minutes is generally called the laning stage.

I usually play alone so in a public match, i.e., non-professional match, the match-making system will randomly match me with 4 teamates and 5 opponents based on my skill level. At my skill bracket, the team coordination and strategy execution are relatively weak. Therefore, whichever team that has the advantage after the laning stage is likely to win the game because the losing team usually can’t play as a team and figure out a strategy to shrink the gap and eventually make a comeback. Given that, my analysis will focus on my performance during the laning stage as a mid-lane player. I will assess my performance by the number of enemy creeps I kill and the number of friendly creeps I deny, item purchase decisions, item usage efficiency, rune controll, and rune usage.

A rune is an object that spawns in the game and there are 7 types of runes in total, the bounty rune, the arcane rune, the haste rune, the double damage rune, the regeneration rune, and the invisability rune. A player can either get his or her hero a temporary boost in power by picking up one of the 6 types of power runes or grant bonus gold to all players on the team by picking up a bounty rune. Runes can spawn at the four locations marked on the map. The top left and bottom right spots only spawn bounty runes. Each of the spots spawns a bounty rune at the beginning of the game, 0:00, and every 5 minutes afterwards. Only one of the middle two spots spawns a power rune. The first power rune spawns at 4:00. Then a new power rune spawns every 2 minutes. Keep in mind that the runes that were not picked up will disappear instead of ramaining on the map when the new runes spawn.

The map of Dota2's battle ground.

Figure 8: The map of Dota2’s battle ground.

Data

The data of my most recent 20 matches were fetched using the httr package in R and the OpenDota (an open source Dota2 data platform) API. The data originally came as JSON files. When parsed using the content function of httr, the data were transformed to a nested list object in R. I wrote several functions to extract data from the list and turn them into tidy datasets. Those functions are contained in my R package, DotaR. More tools will be added to the package later.

The main functions are get_match_id, get_match_id_lane_role, and get_player_data

get_match_id(player_id, n)

Show code

# Get a vector of match id

get_match_id <- function(player_id, n){
  # Check inputs
  if (!is.numeric(player_id)){
    stop('player_id must be numeric')
  }
  if (!is.numeric(n)){
    stop('n must be numeric')
  }
  
  # Call API
  api_call <- GET(paste("https://api.opendota.com/api/players/", player_id, "/matches?limit=", n, sep = ""))
  # Report error if any and stop
  if (status_code(api_call) != 200){
    stop(paste("Error:", httr::status_code(api_call)))
  }
  
  # Parse JSON
  match_id_json <- content(api_call, as = "parsed", type = "application/json")
  num_match <- length(match_id_json)
  # Print out how many games are returned
  cat(paste("\nRequested:", n, ""),
      paste("\nReturned:", num_match))
  
  # Extract match_id
  match_id_vec <- sapply(match_id_json, function(x) return(x$match_id))
  
  # Return match_id
  return(match_id_vec)
}
get_match_id_lane_role(player_id, n, lane_role)

Show code

# Get a vector of match id by lane role

get_match_id_lane_role <- function(player_id, n, lane_role){
  # Check inputs
  if (!is.numeric(player_id)){
    stop('player_id must be numeric')
  }
  if (!is.numeric(n)){
    stop('n must be numeric')
  }
  if (!is.numeric(lane_role)){
    stop('lane_role must be numeric')
  }
  
  # Call API
  api_call <- GET(paste("https://api.opendota.com/api/players/", player_id, "/matches", sep = ""),
                  query = list(limit = n, 
                               lane_role = lane_role))
  # Report error if any and stop
  if (status_code(api_call) != 200){
    stop(paste("Error:", httr::status_code(api_call)))
  }
  
  # Parse JSON
  match_id_json <- content(api_call, as = "parsed", type = "application/json")
  num_match <- length(match_id_json)
  # Print out how many games are returned
  cat(paste("\nRequested:", n, ""),
      paste("\nReturned:", num_match))
  
  # Extract match_id
  match_id_vec <- sapply(match_id_json, function(x) return(x$match_id))
  
  # Return match_id
  return(match_id_vec)
}
get_player_data(player_id, match_vec, wait_time = 1.00, summary_info = TRUE)

Show code

# Get list of data for a specific player
get_player_data <- function(player_id, 
                            match_vec,
                            wait_time = 1.00,
                            summary_info = TRUE){
  # Check input
  if (!is.numeric(player_id)){
    stop('player_id input must be numeric.')
  }
  if (!is.numeric(match_vec)){
    stop('match_vec input must be a numeric vector.')
  }
  
  # Save the inputs for later reference
  player_id <- player_id
  # In case there is any duplicate
  match_vec <- unique(match_vec)
  
  # Initialize a list to store the match data, number of parsed match
  # unparsed match, and error count, number of matches need to be fetched
  player_data_list <- list()
  parsed_match <- 0
  unparsed_match <- 0
  error_count <- 0
  num_match <- length(match_vec)
  
  # Start iteration
  for (i in 1:num_match){
    # Display informative message
    cat(paste("\nFetching", i, "of", num_match))
    
    # Match that need to be fetched
    match_id <- match_vec[i]
    
    # Initialize start time
    start_time <- proc.time()[3]
    
    # Call API
    api_call <- httr::GET(paste("https://api.opendota.com/api/matches/", match_id, sep = ""))
    # Check if we got an error during the call, if so, record the error and move on
    if (httr::status_code(api_call) != 200){
      warning(paste("Error:", httr::status_code(api_call)))
      error_count <- error_count + 1
      api_delay(start_time, wait_time)
      next
    }
    
    # Get JSON
    match_data <- content(api_call, as = "parsed", type = "application/json")
    # If teamfight data is not parsed, we assume the match is not parsed by opendota and move on
    if (is.null(match_data$teamfights)){
      message(paste("Match", match_id, "is not parsed by OpenDota."))
      unparsed_match <- unparsed_match + 1
      api_delay(start_time, wait_time)
      next
    }
    
    # Define a function to extract all player id in the match
    extract_player_id <- function(x){
      if (is.null(x$account_id)){
        x$account_id <- 0
      }
      return(x$account_id)
    }
    
    # Get the data specific to the player 
    players_id <- sapply(match_data$players, extract_player_id)
    player_index <- which(player_id == players_id)
    player_data <- match_data$players[[player_index]]
    
    # Store the JSON in match_data_list
    player_data_list[[i]] <- player_data
    parsed_match <- parsed_match + 1
    api_delay(start_time, wait_time)
  }
  
  # If summary_info == TRUE, print a summary information on how many games 
  # were parsed.
  if (summary_info == TRUE){
    cat(paste("\nTotal matches:", num_match),
        paste("\nTotal parsed:", parsed_match),
        paste("\nTotal unparsed:", unparsed_match),
        paste("\nTotal error", error_count))
  }
  
  # If the user only requested 1 match, return the match JSON directly
  if (num_match == 1){
    return(player_data)
  } else {
    return(player_data_list)
  }
}

Using get_match_id, the user can input his or her account ID and the number, n, of games desired to return a numeric vector of length n containing the match ID’s of the n most recent matchs. get_match_id_lane_role takes an additional argument land_role so it only returns matchs played in a specific lane. For example, in my case, I only want 20 most recent matchs where I played the mid lane so I would set get_match_id_lane_role(my_account_id, n = 20, lane_role = 2). After getting a vector of match ID’s, the user can pass his or her account ID and the match ID vector into get_player_data. The function will return a nested list object containing n lists. Each of those n lists contains the data of each match.

With the list returned by get_player_data, the user can use the helper function get_lh_dn_df to get a dataframe recording the culmulative number of creeps killed and denied at each minute; get_purchase_log_df to get a dataframe recording the items bought and the time at which they were bought; get_kill_log_df to get a dataframe recording enemy heroes killed and the time at which they were killed; get_rune_log_df to get a dataframe recording the the runes obtained and the time at which they were obtained.

get_lh_dn_df(player_data)

Show code

# Get a dataframe recording the culmulative number of creeps killed and denied at each minute
  
get_lh_dn_df <- function(player_data){
  # Check input
  if (!is.list(player_data)){
    stop('player_data must be the list output by get_player_data().')
  }
  
  # Initialize a list to store lh_dn_df for each match
  lh_df_list <- list()
  
  # Construct data frame
  for (i in 1:length(player_data)){
    lh_dn_df <- data.frame(player_id = rep(player_data[[i]]$account_id, length(player_data[[i]]$lh_t)),
                           match_id = rep(player_data[[i]]$match_id, length(player_data[[i]]$lh_t)),
                           date = rep(player_data[[i]]$start_time, length(player_data[[i]]$lh_t)),
                           in_game_time = seq(0, length(player_data[[i]]$lh_t) - 1),
                           last_hit = sapply(player_data[[i]]$lh_t, function(x) return(x)),
                           deny = sapply(player_data[[i]]$dn_t, function(x) return(x))) %>%
      mutate(date = date(as_datetime(date)))
    
    # Append dataframe to the list
    lh_df_list[[i]] <- lh_dn_df
  }
  # Merge all the datasets in the list
  lh_dn_df <- reduce(lh_df_list, full_join)
  
  # Return the dataset
  return(lh_dn_df)
}
get_purchase_log_df(player_data)

Show code

# Get a dataframe recording the items bought and the time at which they were bought

get_purchase_log_df <- function(player_data){
  # Check input
  if (!is.list(player_data)){
    stop('player_data must be the list returned by get_player_data().')
  }
  
  # Initialize a list to store purchase_log_df 
  purchase_log_list <- list()
  
  # Get purchase log
  for (i in 1:length(player_data)){
    n_rows <- length(player_data[[i]]$purchase_log)
    purchase_log_df <- data.frame(player_id = rep(player_data[[i]]$account_id, n_rows),
                                  match_id = rep(player_data[[i]]$match_id, n_rows),
                                  date = rep(player_data[[i]]$start_time, n_rows),
                                  in_game_time = sapply(player_data[[i]]$purchase_log, function(x) return(x$time)),
                                  item = sapply(player_data[[i]]$purchase_log, function(x) return(x$key))) %>%
      mutate(date = date(as_datetime(date)),
             item = as.character(item))
    
    # Append dataframe to purchase_log_list
    purchase_log_list[[i]] <- purchase_log_df
  }
  
  # Merge all dateframe in list
  purchase_log_df <- reduce(purchase_log_list, full_join)
  
  # Return purchase_log_df
  return(purchase_log_df)
}
get_kill_log_df(player_data)

Show code

# Get a dataframe recording enemy heroes killed and the time at which they were killed

get_kill_log_df <- function(player_data){
  # Check input
  if (!is.list(player_data)){
    stop('player_data must be the list returned by get_player_data().')
  }
  
  # Initialize a list to store kill_log_df
  kill_log_list <- list()
  
  # Get kill log
  for (i in 1:length(player_data)){
    n_rows <- length(player_data[[i]]$kills_log)
    kill_log_df <- data.frame(player_id = rep(player_data[[i]]$account_id, n_rows),
                              match_id = rep(player_data[[i]]$match_id, n_rows),
                              date = rep(player_data[[i]]$start_time, n_rows),
                              in_game_time = sapply(player_data[[i]]$kills_log, function(x) return(x$time)),
                              kill = sapply(player_data[[i]]$kills_log, function(x) return(x$key))) %>%
      mutate(date = date(as_datetime(date)),
             kill = as.character(kill),
             kill = str_extract_all(kill, "(?<=npc_dota_hero_)[:graph:]+"),
             kill = str_replace_all(kill, "_", " "))
    
    # Append dataframe to kill_log_list
    kill_log_list[[i]] <- kill_log_df 
  }
  
  # Merge all dataframes in list
  kill_log_df <- reduce(kill_log_list, full_join)
  
  # Return kill_log_df
  return(kill_log_df)
}
get_rune_log_df(player_data)

Show code

# Get a dataframe recording the the runes obtained and the time at which they were obtained
get_rune_log_df <- function(player_data){
  # Check input
  if (!is.list(player_data)){
    stop('player_data must be the list given by get_player_data().')
  }
  
  # Initialize a list to store rune_log_df for each match
  rune_log_list <- list()
  
  # Get rune log
  for (i in 1:length(player_data)){
    n_rows <- length(player_data[[i]]$runes_log)
    rune_log_df <- data.frame(player_id = rep(player_data[[i]]$account_id, n_rows),
                              match_id = rep(player_data[[i]]$match_id, n_rows),
                              date = rep(player_data[[i]]$start_time, n_rows),
                              in_game_time = sapply(player_data[[i]]$runes_log, function(x) return(x$time)),
                              rune = sapply(player_data[[i]]$runes_log, function(x) return(x$key))) %>%
      mutate(date = date(as_datetime(date)),
             rune = case_when(rune == 0 ~ "double damage",
                              rune == 1 ~ "haste",
                              rune == 2 ~ "illusion",
                              rune == 3 ~ "invisibility",
                              rune == 4 ~ "regenaration",
                              rune == 5 ~ "bounty",
                              rune == 6 ~ "arcane"))
    
    # Append dataframe to the list
    rune_log_list[[i]] <- rune_log_df
  }
  
  # Merge all the dataframes in the list
  rune_log_df <- reduce(rune_log_list, full_join)
  
  # Return
  return(rune_log_df)
}

My analysis was done using all 4 dataframes. All datasets also record the player ID, the match ID’s, and the dates when the matchs were played. To give an example, let’s look at a snippet of the kill log.

Player ID Match ID Date In game time (s) Kill
55647616 5429624203 2020-05-22 643 beastmaster
55647616 5429624203 2020-05-22 726 beastmaster
55647616 5429624203 2020-05-22 1036 crystal maiden
55647616 5429342611 2020-05-22 815 beastmaster
55647616 5429342611 2020-05-22 964 grimstroke
55647616 5429342611 2020-05-22 1231 alchemist
55647616 5429342611 2020-05-22 1467 grimstroke
55647616 5429342611 2020-05-22 1574 alchemist
55647616 5429342611 2020-05-22 1811 beastmaster
55647616 5429342611 2020-05-22 1860 lion

Analysis

My analysis will focus on my performance as a mid-lane player. During the laning stage, i.e., the first 10 minutes of the game, the battle of the mid lane is the most intense in comparison with other lanes since it is a 1 versus 1 situation. Whether you win the lane or not largly comes down to your level of mastery of the basic game mechanism, understanding the strengths and weaknesses of both your own hero and the opponent hero, resource management (what items to purchase with your hard-earned gold to help you win the lane), decision making (whether you want to help other lanes at the risk of not getting anything out of it while losing the gold and experience you could have gotten in lane), and your ability to quickly adapt to different situations. My analysis will focus on the two most basic aspects of wining the lane, mastery of basic mechanism and resource management.

Lane Efficiency

One of the most fundamental and basic mechanism of Dota2 is killing lane creeps, neutral creeps, and denying lane creeps to accumulate gold and experience yourself while preventing the opponent from getting the gold and experience. We will first look at the consistency of my ability to secure creep kills and denies during the laning stage. The number of creep kills and denies for each of those 20 games is shown in the figures below.

This figure shows both the cumulative creeps kills and denies for each of my most recent 20 matchs. Match 1 is the most recent match. Match 20 is the least recent match. A triangle represents the number of creep kills. A dot represents the number of denies. Red points represent a win. Black points represent a loss.

(#fig:lh_dn)This figure shows both the cumulative creeps kills and denies for each of my most recent 20 matchs. Match 1 is the most recent match. Match 20 is the least recent match. A triangle represents the number of creep kills. A dot represents the number of denies. Red points represent a win. Black points represent a loss.

As we can see my numbers of creep kills and denies were fairly consistent across the 20 matches, which means my performance was at least fairly consistent. However, were they consistenly bad, consistenly good, or consistenly average? To assess that, we need to revisite the creep spawning mechanism.

Recall that a group of creeps consists of 4 creeps in total. The first wave of creeps spawns at 0:00. Then, a new wave will spawn every 30 seconds. A simple caculation can show that, cumulatively and combining both Radient and Dire creeps, there would be 160 creeps going down each lane by 10:00, the end of laning stage. Therefore, in theory, a player could get 80 creep kills and 80 denies in lane.

However, that’s not possible in reality since players from the two teams would compete for creep kills and denies. Human are not robots (Actually, when competing with human players, even AI can’t get an 80/80 creep kills/denies). Even if one player is significantly better than the enemy player, the better player would still miss a portion of creep kills and denies due to human error and other factors such as leaving the lane to get a rune or leaving the lane to help teamates. Moreover, a deny is harder to get than a creep kill in general. Therefore, we shouldn’t expect an even distribution for creep kills and denies.

On the other hand, the neutral creeps spawns at each full minitue mark, excluding 0:00, assuming they are cleared by players each time. But in reality neutral creeps are usually not cleared every time. This mechanism makes the analysis a little bit tricky because we can’t reach a common number of total neutral creeps spawned for every match like the lane creeps. Moreover, the data doesn’t not distinguish lane creep kills and neutral creep kills. Therefore, it is better to aggregate lane creep kills and neutral creeps kills together as a single measure.

Following the practice of OpenDota, let’s assume the average highest realistic creep kills for a mid-lane player is 80 and the average highest realistic denies for a mid-lane player is 20. Let’s define lane efficiency:

A figure of my lane efficiency with the above assumptions is shown here:

This figure shows my lane efficiency of my 20 most recent matchs. Each dot represents the lane efficiency of a match. Red dot represents a win. Black dots represents a loss. The dotted line marks the average lane efficiency.

Figure 9: This figure shows my lane efficiency of my 20 most recent matchs. Each dot represents the lane efficiency of a match. Red dot represents a win. Black dots represents a loss. The dotted line marks the average lane efficiency.

As we can see, for the majority of the matchs, my lane efficiency is consistently around 60.45% ranging from 50% to 70%, which means most of my performance is above the average. However, notice that there are 4 matchs where my lane efficiencies are below the average. Those four matchs share the same issue — I had a disadvantagous match-up, meaning the opponent hero’s strength matched my hero’s weakness so it was very hard for me to get lane creep kills and denies in lane. In order to improve, I should work on my understanding of the different match-up’s so I wouldn’t pick a hero that is bad against the opponent hero if I pick after the opponent player in the draftin stage, and practice to improve my mastery of the basic mechanism to ensure that I would not miss the lane creep kills and denies due to my error in addition to the already difficult match-up.

Another side of the consistency is that it signals that I have reached a plateau in my performance. The consistency is the call for change in general since my current stragety and skill don’t make me improve progressively and I am aware that there is plenty room for improvement.

Item Purchase and Usage

For the sake of simplicity, I will only analyze the purchase and usage for consumable regeneration items. A consumable regeneration item can be consumed to regenerate a hero’s health and mana. A hero’s health determines the maximum damage the hero can take before the hero dies. For example, ember spirit has 620 health to begin with, which means when an enemy hero deals 620 damage to ember spirit, he dies. A hero’s mana is consumed to use abilities. Using ember spirit as an example again, he starts with 315 mana. One of his abilities, sleight of fist, costs 50 mana to use once. Therefore, he can use that ability 6 times in total before he needs to regnerate his mana. The regeneration effect of some consumable regeneration items can be distrupted if your hero receives damage from a enemy hero. A table of all the comsumable regeneration items that can be purchased is presented here:

Item Regen per Charge Duration per Charge Charge Price Disruptable
Tango 115 Health 16s 3 90 No
Healing Salve 400 Health 10s 1 110 Yes
Faerie Fire 85 Health Instant 1 70 No
Clarity 180 Mana 30s 1 50 Yes
Enchanted Mango 110 Mana Instant 1 70 No
Bottle 125 Health, 75 Mana 2.5s 3 625 Yes

The last item, the bottle, is special because when all three charges are consumed, the bottle doesn’t disappear and players can replenish the 3 charges of the bottle by getting any kind of runes. You can think the bottle as a rechargable battery and the rune as a charging station. Another special thing about the bottle is that a player can use it to bottle a rune and use the rune later as oppose to consuming the rune immediately at the moment the player pick it up. This function has very important strategical value but I won’t dive into that here because I can wrote an entire blog on that. Instead, I will treat the bottle as a pure regeneration item.

The bottle is frequently bought by mid-lane players becuase the mid lane is closest to the two middle rune spawn sites that spawns a rune at 4:00 and every 2 minutes afterwards. Theoratically, if a player gets every power rune, 4 in total, and every bounty rune, except the 0:00 one, that spawns on the friendly side (marked by yellow points in the map), 2 in total, of the map in the first 10 minutes, the player would have 18 charges in total that can provide 2250 health regen and 1350 mana regen in total, assuming the regeneration effect was never distrupted.

Map.

Figure 10: Map.

However, you have to really dominate your lane to be able to get all those runes and that rarely happens. Moreover, it is unrealistic to assume that the regeneration effect was never disrupted. To analyze my bottle purchase decision and usage, We will look at a timeline of my bottle purchase and rune acquisition for one of the match where I purchased a bottle. When comparing the value of the bottle to other regneration items, I will make the assumption that the regneration effect of a distruptable item lasts 80% of the duration.

A timeline indicating when I purchased the bottle and when I got each rune.

Figure 11: A timeline indicating when I purchased the bottle and when I got each rune.

By the end of the laning stage, I got 2 runes in total. Therefore, combining with the orignal 3 charges that came with the bottle, I had 9 charges. Based on our assumptions, each charge can regenerate 100 health and 60 mana, which makes the regeneration 900 health and 540 mana in total. We will compare the cost-effectiveness of the bottle, tango, healing salve, clarity and enchanted mango. The numer of each item required to regenerate 900 health and 540 mana and their respective cost is listed in the following table. Sine items can not be bought in portions, the quantity required is round up when necessary. For example, if 2.5 Healing Salve is required, we will make it 3.

Item Quantity Total Regeneration Total Duration Cost
Tango 3 1035 Health 144s 270
Healing Salve 3 960 Health 30s 330
Clarity 4 576 Mana 120s 200
Enchanted Mango 5 550 Mana Instant 350
Bottle 1 900 Health, 540 Mana 22.5s 625

To match the same amount of regeneration of the bottle, the best combiantion I could have got in terms of price is 3 sets of tango, 4 clarities, and 3 Healing Salve, 4 clarities. Although the combination of the tango and the clarity could be significantly cheaper and regenerate more health and mana, the amount of time it takes to complete the regeneration process makes this combination an invalid option since the regeneration rate per second of this combination is too low to keep up with the intense action of the mid lane. On the other hand, the combination of the healing salve and the clarity is 95 gold cheaper than the bottle, which might make this combination a valid purchase when I am in a match-up that is too hard, or even impossible, to get runes. However, the total regneration time required by the clarity, 120s, is significantly longer than that of the bottle, 22.5s, which would make it more likely for the enemy to distrupt my regeneration.

In my analysis, I made the assumption that all items is consumed to 80% of their maximum regeneration. However, the longer the regneration time per charge, the more likely it is for the enemy to disrupt the regneration effect. Therefore, in reality, 80% would be an over estimate for the amount of the healing salve and the clarity that is usually consumed. Even in the match represented in the above figure, where I only got two runes, the bottle had the advantage of fastest regneration time and the ability to bottle a rune for later consumption, with a fairly good price in comprison to other items. Moreover, the bottle becomes more valuable the more runes I get. Hence, as a mid-lane player, it should be always worth it to purchase a bottle unless I had a match-up where I have no hope of getting any rune at all. In that case, I might still purchase a bottle after the laning stage. As you can see in the timeline, I got 9 runes after the laning stage. The amount of regneration from the 9 runes alone made the bottle pay off itself, not to mention its stratigical value of bottling the rune.

Conclusion

There are many other possible analysis can be done using the data. For the purpose of this blog, the analysis I’ve presented is rather preliminary. However, the analysis still reveals a lot valuable information. Through the analysis of lane efficiency, I realized that I need to improve my knowledge on different match-up’s and work on basic mechanism especially in disadvantageous match-up’s. Through the analysis of the value of the bottle, I found the answer to the question that I’ve asked myself many times, “Is the bottle worth it in this match?”. In comparison with analyzing the replay video of each match, the data oriented approach can help me understand my performance in a broader scale since all the data can be presented and analyzed at the same time. While analyzing replay video focuses more on a particular match. I am interested to see how much I can improve if I combine these two methods toghter and hopefully I will write another blog to report my results in the future.

Reference

  1. Title image, louissry

  2. Tower image, Dota 2 Wiki

  3. Lane creep image, Dota 2 Wiki

  4. Neutral creep image, Dota 2 Wiki