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.