9 min read

Clicker Heroes Simulation, Part 2

Introduction

In part 2 of my series to create a Idle Game simulation, I’ll be adding in the ability to upgrade hero damage ouput with multipliers. View part 1 to see how I created the basic simulation framework.

So far we have a simulation of the damage, gold, monster killing and heroes. Yet, we are still missing a major tool for balancing; Multipliers. In Clicker Heroes, upgrades are purchasable when a hero reaches a certain level. These upgrades most often impact the hero’s damage per second by adding a static multiplier to a single hero’s damage output, but can also serve to change all hero DPS or even directly impact gold.

Getting Upgrade Information

I’ll be continuing this simulation in R. These are the packages we’ll need. The clicker_heroes_sim.R file contains the functions we created in part 1

library(rvest) # parse html
library(tidyverse) # make R fun
library(tidytext) # text functions
source("clicker_heroes_sim.R") # custom simulation functions

First, we need data to create upgrades. With a bit of web scraping we can avoid having to manually enter in the up upgrades used in Clicker Heroes. From the wiki page, I’m able to scrape all of the upgrade tables.

clicker_heroes_upgrades_page <- read_html("https://clickerheroes.fandom.com/wiki/Upgrades")

upgrade_tables <- clicker_heroes_upgrades_page %>%
  html_nodes("table") %>%
  html_table()

One trouble with web scrapping is that code tends to break when websites change their formatting. It’s a good idea to save locally Since this is a static set of data and I don’t want it to break in the future.

saveRDS(upgrade_tables, "../data/upgrade_scraped_tables.rds")
upgrade_tables <- readRDS("../data/upgrade_scraped_tables.rds")

Here’s an example of what a table of data looks like. I’m starting on 2nd index because there is a table used for navigation buttons before the upgrade tables.

upgrade_tables[[2]]
##             Upgrade Icon Unlocks
## 1        Big Clicks   NA  Lvl 10
## 2        Clickstorm   NA  Lvl 25
## 3       Huge Clicks   NA  Lvl 50
## 4    Massive Clicks   NA  Lvl 75
## 5    Titanic Clicks   NA Lvl 100
## 6   Colossal Clicks   NA Lvl 125
## 7 Monumental Clicks   NA Lvl 150
##                                                          Effect
## 1 Increases Cid, the Helpful Adventurer's Click Damage by 100%.
## 2                                 Unlocks the Clickstorm skill.
## 3 Increases Cid, the Helpful Adventurer's Click Damage by 100%.
## 4 Increases Cid, the Helpful Adventurer's Click Damage by 100%.
## 5 Increases Cid, the Helpful Adventurer's Click Damage by 150%.
## 6 Increases Cid, the Helpful Adventurer's Click Damage by 200%.
## 7 Increases Cid, the Helpful Adventurer's Click Damage by 250%.
##                                                                                                                                                             Description
## 1                                                                  Cid has the unique ability to strengthen your clicks. Upgrade her to make your clicks more powerful.
## 2 "I was surprised too, but it turns out we can upgrade these even further" Cid says. "I suspect you've gotten tired of clicks, though. Maybe you don't need this one?"
## 3                                                                                                 Cid's unique ability can grow in power, allowing you to click harder.
## 4                                                                          "We make a great team", Cid says. "Let's get this upgrade and we'll be an even better team."
## 5                                                                                     Cid looks off into the distance and wonders, "What could be bigger than Titanic?"
## 6                                     "Colossal clicks are bigger than Titanic clicks. We should get this one. I don't think there could be anything bigger", Cid says.
## 7                                                        The final upgrade. These clicks are so big that every time you click, they build a monument to commemorate it.
##      Cost
## 1     100
## 2     250
## 3    1000
## 4    8000
## 5   80000
## 6  400000
## 7 4000000

The web-scrapped data isn’t in the exact format that we can easily code into a simulation. All the needed formatting can be applied to each table with the function below.

get_hero_upgrades <- function(name, upgrade_df){
  type <-  vector("character", nrow(upgrade_df))
  #if contains word "Unlock", becomes skill
  type[!is.na(str_match(upgrade_df$Effect, "(Unlock)")[,2])] <- "skill"
  #if contains words "Click Damage", becomes click
  type[!is.na(str_match(upgrade_df$Effect, "(Click Damage)")[,2])] <- "click"
  #if contains words "all heroes", becomes all_heroes
  type[!is.na(str_match(upgrade_df$Effect, "(all heroes)")[,2])] <- "all_heroes"
  #if contains words "Critical Click Chance", becomes critical_click_chance
  type[!is.na(str_match(upgrade_df$Effect, "(Critical Click Chance)")[,2])] <- "critical_click_chance"
  #if contains words "all gold found", becomes gold
  type[!is.na(str_match(upgrade_df$Effect, "(all gold found)")[,2])] <- "gold"
  #if contains own name, becomes self
  type[!is.na(str_match(upgrade_df$Effect, paste0("(",name, ")"))[,2])] <- "self"
  
  #get any numbers in the effect text
  value <- as.numeric(str_match(upgrade_df$Effect, "[0-9/.]+")[,1])/100
  cost <- as.numeric(upgrade_df$Cost)
  level <-  as.numeric(str_match(upgrade_df$Unlocks, "[0-9]+")[,1])
  upgrade_name <- upgrade_df$Upgrade

  cleaned_upgrade_df <- data.frame(hero_name = name,
                         upgrade_name = upgrade_name,
                         cost = cost,
                         level = level,
                         type = type,
                         value = value,
                         purchased = FALSE)
  
  cleaned_upgrade_df
}

You’ll notice that there is no hero name in the table data. I’ll have to manually specify the names. Luckily we have a list from part 1 we can use. Cid was removed from that table for that table, so I’ll add her back in.

hero_costs_df <- read.csv("../data/clicker_heroes_cost.csv", 
                          header=FALSE, 
                          stringsAsFactors = FALSE)
names(hero_costs_df) <- c("hero_name", "base_cost", "base_damage")
hero_names <- c("Cid, the Helpful Adventurer", hero_costs_df$hero_name)

All that is left is to get the upgrades for each hero by applying get_hero_upgrades to each table that was web-scrapped.

hero_upgrades_df <- data.frame()
for(i in 1:length(hero_names)){
  name <- hero_names[i]
  upgrade_df <- upgrade_tables[[i + 1]]
  hero_upgrades_df <- rbind(hero_upgrades_df, get_hero_upgrades(name, upgrade_df ))
}
head(hero_upgrades_df)
##                     hero_name    upgrade_name   cost level  type value
## 1 Cid, the Helpful Adventurer      Big Clicks    100    10  self   1.0
## 2 Cid, the Helpful Adventurer      Clickstorm    250    25 skill    NA
## 3 Cid, the Helpful Adventurer     Huge Clicks   1000    50  self   1.0
## 4 Cid, the Helpful Adventurer  Massive Clicks   8000    75  self   1.0
## 5 Cid, the Helpful Adventurer  Titanic Clicks  80000   100  self   1.5
## 6 Cid, the Helpful Adventurer Colossal Clicks 400000   125  self   2.0
##   purchased
## 1     FALSE
## 2     FALSE
## 3     FALSE
## 4     FALSE
## 5     FALSE
## 6     FALSE

The first simulation incremented damage per second by adding onto the current rate. E.G dps <- dps + 1. This method becomes much harder to calculate with damage multipliers. Instead we’ll calculate the damage each time from the hero_tracker_df.

hero_tracker_df <- hero_costs_df
hero_tracker_df$level <- 0
hero_tracker_df$multiplier <- 1
head(hero_tracker_df)
##                   hero_name base_cost base_damage level multiplier
## 1                 Treebeast        50           5     0          1
## 2 Ivan, the Drunken Brawler       250          22     0          1
## 3  Brittany, Beach Princess      1000          74     0          1
## 4   The Wandering Fisherman      4000         245     0          1
## 5             Betty Clicker     20000         976     0          1
## 6        The Masked Samurai    100000        3725     0          1
get_hero_dps <- function(hero_tracker_df){
  hero_tracker_df$base_damage * hero_tracker_df$level * hero_tracker_df$multiplier
}

apply_upgrades is used to get the upgrades that are available to purchase and calculate how they affect DPS. In this post, I simplify this process by only using upgrades that impact a single hero.

#also include other multiplier list in here, click, gold, etc
apply_upgrades <- function(hero_tracker_df, hero_upgrades_df){
  merged_df <- merge(hero_tracker_df, hero_upgrades_df, by = "hero_name")
  
  #get hero_df with multipliers
  purchasable_merged_df <- merged_df %>% 
    filter(
      level.x == level.y, 
      purchased == FALSE) %>%
    mutate(
      upgrade_multiplier = case_when(
        type  == "self" ~ value/100,
        TRUE ~ 0)
      ) %>%
    mutate(
      multiplier = multiplier + upgrade_multiplier
    ) %>%
    rename(
      level = level.x
    )
  #calcualte dps        
  dps_vec <- get_hero_dps(purchasable_merged_df)
  
  purchasable_merged_df$dps <- dps_vec
  purchasable_merged_df$purchase_type <- "upgrade"
  #return selected columns
  purchasable_merged_df %>%
    select(
      upgrade_name,
      cost,
      dps,
      purchase_type
    )
}

After a few modifications of the simulation code written in part 1, we can see how upgrade multipliers impact the game. If you want to view those modifications, I’ve included the script on GitHub

Simulation Without Upgrades

system.time(simulation_results_no_upgrades <- simulate_game(10000, 1, 1, hero_tracker_df, hero_upgrades_df[0,]))
saveRDS(simulation_results_no_upgrades, "sim_results_no_upgrades_10000.rds")
user system elapsed
3257.52 32.33 3295.31

Simulation With Upgrades

system.time(simulation_results_with_upgrades <- simulate_game(10000, 1, 1, hero_tracker_df, hero_upgrades_df))
saveRDS(simulation_results_with_upgrades, "sim_results_with_upgrades10000.rds")
simulation_results_with_upgrades
user system elapsed
3592.77 23.70 3620.44

We can combine both of these simulations to make graphs in ggplot.

stats_no_upgrades <- simulation_results_no_upgrades$stats_df %>%
  add_column(includes_upgrades = FALSE)
stats_with_upgrade <- simulation_results_with_upgrades$stats_df %>%
  add_column(includes_upgrades = TRUE)
stats_merged <- rbind(stats_no_upgrades, stats_with_upgrade) %>% arrange(time)
## Warning: package 'bindrcpp' was built under R version 3.4.4

Viewing the DPS over time, you can see how the multipliers help keep pace with the log scale over the first couple of hours.

stats_merged  %>%
  filter(time < 7200) %>%
  ggplot(aes(x = time/60, y = dps)) +
  geom_line(aes(color = includes_upgrades)) +
  scale_y_log10() + 
  labs(title = "Total DPS Over Time",
       x = "Time (minutes)",
       y = "DPS")

Over a longer period of time the upgrade multipliers lose effectiveness. This makes sense as Clicker Heroes adds in many other multipliers that to keep progression up. At 4000 hours, this method of only using heroes and their upgrade multipliers would be unplayable.

stats_merged  %>%
  ggplot(aes(x = time/3600, y = dps)) +
  geom_line(aes(color = includes_upgrades)) +
  scale_y_log10() + 
  labs(title = "Total DPS Over Time",
       x = "Time (hours)",
       y = "DPS")

Gold mirrors damage very closely.

stats_merged %>% 
  ggplot(aes(x = time/3600, y = gold)) +
  geom_line(aes(color = includes_upgrades)) +
  scale_y_log10() + 
  labs(title = "Total Gold Per Second Over Time",
       x = "Time (hours)",
       y = "Gold")

We can also see the importance of additional multipliers and prestige by looking at the zone progression over time. After 4000 hours we only get to zone 75. Progression is capped by the need to defeat bosses in 30 seconds. In active play you could get past these blocks by using special moves which temporarily increase damage.

stats_merged %>% 
  ggplot(aes(x = time/3600, y = zone)) +
  geom_line(aes(color = includes_upgrades)) +
  labs(title = "Zone Progression Over Time",
       x = "Time (hours)",
       y = "Zone")

One other thing I wanted to look at is how the damage per hero changes when adding in multipliers.

heroes_purchased_no_upgrades <- simulation_results_no_upgrades$heroes_purchased_df %>%
  add_column(includes_upgrades = FALSE)
heroes_purchased_with_upgrade <- simulation_results_with_upgrades$heroes_purchased_df %>%
  add_column(includes_upgrades = TRUE)

Without upgrades, we see steady increases for each hero.

heroes_purchased_no_upgrades %>%
  filter(time < 40000) %>%
  ggplot(aes(x = time/60, y = hero_damage)) + 
  geom_line(aes(color = hero_name)) + 
  scale_y_log10() +
  labs(title = "Percent Damage Dealt Per Hero",
       x = "Time (minutes)",
       y = "Percent of Total Damage Per Second")

With multipliers we can see some heroes overtaking others when their upgrade is cost. Having this switch up of how much damage each hero deals makes fore a more interesting optimization problem.

heroes_purchased_with_upgrade %>%
  filter(time < 40000) %>%
  ggplot(aes(x = time/60, y = hero_damage)) + 
  geom_line(aes(color = hero_name)) + 
  scale_y_log10() +
  labs(title = "Percent Damage Dealt Per Hero",
       x = "Time (minutes)",
       y = "Percent of Total Damage Per Second")

By adding multipliers we can start to see how Clicker Heroes becomes more interesting with additional mechanics. I won’t be able to code every mechanic in this simulation, but if you are interested in working on that I’ve created a code repository on GitHub. In the next part I’m going to work to improve performance and generalize some of the mechanics for other Idle games. Currently, this simulation takes an hour to compute 10,000 actions. I’m aiming to get something fast enough to use a shiny application.