Star Wars: Galaxy of Heroes – who knew a mobile game could be so complicated?!

[This article was first published on R on R-house, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

How I started playing

Back in 2015, during the boring period between Christmas and New Year, I decided to try out a Star Wars mobile game. Star Wars: Galaxy of Heroes is essentially a jazzed up version of Top Trumps where you collect characters, upgrade them, and use them to earn rewards.

What started out as a curiosity ended up being quite an addictive hobby, and the number of features and game modes (and new characters to unlock!) grew quickly. After quitting the game due to the massively disruptive introduction of “mods”, which allowed you to finely tune the stats of your characters, I returned to the game 9 months later to find that the developers had reduced the game destroying effects they had initially caused.

It was around this time that I was beginning to learn R, and was looking for a personal project to increase my competence. Fortunately, SWGOH is a very data intensive (but unfortunately time intensive) game, and the player community was screaming out for some tools to help them plan and manage their resources. Key among the tools that players were crying out for was some kind of mod management utility.

What are mods??

Before I get into mods, it’s probably worth explaining some things. Firstly, this game is highly competitive (and addictive). One of the game modes allows you to pit a team of 5 of your chosen characters against other people’s teams, with a ranking system that gives various rewards, including crystals. Crystals are the base currency of the game and actually have a monetory value, at least in so far as it’s crystals that are bought by Pay-to Win players. Therefore this particular game mode is extremely important for players that want to progress quickly. Winning in this game mode depends on the abilities and stats of your team members, and a general rule of thumb is speed is king, as it means your characters can take more turns than your opponents’.

Speed isn’t the only stat that characters have. In facts there’s a whole array of different stats including physical damage, special damage, potency, and tenacity, and some of the interactions of these stats when in battle can be rather complex. The introduction of mods allowed players to enhance particular stats to magnify particular strengths of their characters (or toons) or bolster their weaknesses. Each toon has six slots in which to put mods, each of which is a particular shape, and will only take mods of the shape. In fact mods have several different features, which makes the whole thing annoyingly complex:

  • Shape: Square, Diamond, Circle, Arrow, Triangle, or Cross;
  • Primary stat enhancement: A big boost for a particular stat;
  • Secondary stat enhancements: An additional little boost for up to 4 stats;
  • Level: Spending in-game credits to increase the level of the mod (max level 15) to increase the stat enhancements;
  • Pips: Each mod has 1 to 5 pips signifying the quality of the mod; the higher the number of pips, the bigger the potential enhancements;
  • Set Bonus: An association with a particular stat. If you use a set number of mods of a particular stat association, you gain a bonus increase in that stat (which is increased even more if all the mods in that set are max level 15).

Phew!! The common problem players would have would be an inventory of hundreds of mods, dozens of toons to put them on, and finding the time to decide which were the best mods to put on which toons…definitely a need for some automation here! If you’re interested, there’s a YouTube video (and others) explaining mods here.

Laying the groundwork

Before I get into how I tried to tackle the problem (and I attempted several methods), it’s probably worth first getting the relatively straightforward stuff out of the way and show how I codified the various rules and reference values that my script would use.

First off, I made use of the tidyverse package, and began defining some names:

library(tidyverse)

slots <- c("Square","Diamond","Circle",
           "Arrow","Triangle","Cross")
           
stat_names <- c("Speed", 
                "Speed %", 
                "Potency %",
                "Tenacity %",
                "Offense",
                "Offense %",
                "Protection",
                "Protection %",
                "Critical Chance %",
                "Critical Damage %",
                "Defense",
                "Defense %",
                "Health",
                "Health %")

Next I build up a dataframe which holds the maximum theoretical enhancements possible for each stat (I planned to use this to do some normalisation later so I could compare like with like). So for example, the maximum Speed enhancement you can get from a primary stat on any one mod is +30, similarly for Offense % is +5.88%. However the max_nprim vector contains the maximum number of mods a toon can hold with that primary stat enhancement, so it’s only one for Speed, but four for Offense %, which means an enhancement of 23.52% is possible for primary stat enhancements alone. Whilst the maximum primary enhancements were quite easy to get, there is more randomness in the secondary stats, so I basically had to base the figures off the maximum values I had ever seen. The maximum set bonuses are always additional percentage increases.

max_prim <- c(30,0,24,24,0,5.88,0,23.5,12,36,0,11.75,0,5.88)
max_nprim <- c(1,0,1,1,0,4,0,4,1,1,0,4,0,4)
max_sec <- c(27,0,9.63,10.19,201,2.44,3630,9.74,10.14,0,41,7.27,1916,5.01)
max_nsec <- c(5,0,6,6,6,6,6,6,6,0,6,6,6,6)
max_set_bonus <- c(0,10,30,30,0,10,0,0,15,30,0,15,0,15)

max_stats <- data.frame(Max.Primary = max_prim * max_nprim,
                        Max.Secondary = max_sec * max_nsec,
                        Max.Set.Bonus = max_set_bonus,
                        row.names = stat_names,
                        stringsAsFactors = FALSE)

rm(max_prim, max_nprim, max_sec, max_nsec, max_set_bonus)
max_stats
##                   Max.Primary Max.Secondary Max.Set.Bonus
## Speed                   30.00        135.00             0
## Speed %                  0.00          0.00            10
## Potency %               24.00         57.78            30
## Tenacity %              24.00         61.14            30
## Offense                  0.00       1206.00             0
## Offense %               23.52         14.64            10
## Protection               0.00      21780.00             0
## Protection %            94.00         58.44             0
## Critical Chance %       12.00         60.84            15
## Critical Damage %       36.00          0.00            30
## Defense                  0.00        246.00             0
## Defense %               47.00         43.62            15
## Health                   0.00      11496.00             0
## Health %                23.52         30.06            15

Next, I define how many mods are needed to obtain the set bonuses (the zeroes mean that there are no set bonuses for that stat). The lower (and upper) vector gives the % enhancement in that stat if you reach that number (and if all of the mods in that set are at maximum level)

set_bonus_n <- c(0,4,2,2,0,4,0,0,2,4,0,2,0,2)
set_bonus_lower <- c(0,5,5,5,0,5,0,0,2.5,15,0,2.5,0,2.5)
set_bonus_upper <- c(0,10,10,10,0,10,0,0,5,30,0,5,0,5)

set_bonus_rules <- data.frame(Number = set_bonus_n,
                              Bonus = set_bonus_lower,
                              Max.Bonus = set_bonus_upper,
                              row.names = stat_names,
                              stringsAsFactors = FALSE)

rm(set_bonus_n, set_bonus_lower, set_bonus_upper)
set_bonus_rules
##                   Number Bonus Max.Bonus
## Speed                  0   0.0         0
## Speed %                4   5.0        10
## Potency %              2   5.0        10
## Tenacity %             2   5.0        10
## Offense                0   0.0         0
## Offense %              4   5.0        10
## Protection             0   0.0         0
## Protection %           0   0.0         0
## Critical Chance %      2   2.5         5
## Critical Damage %      4  15.0        30
## Defense                0   0.0         0
## Defense %              2   2.5         5
## Health                 0   0.0         0
## Health %               2   2.5         5

Reading in game data

This was the tricky bit. There is a website that players use to sync their game accounts with, swgoh.gg, and somehow they had managed to reverse engineer the game to allow users to sync their game data with the website. Unfortunately no API was available, so there was really only one option that I could see - web scraping. I had a go, but didn’t get anywhere and realised this was beyond my skills for now, so a very kind individual behind the website Crouching Rancor sent me a json file containing sample data of the mods contained within his game account.

I imported this file and constructed a dataframe containing all the game data I should need to complete my script.

library(jsonlite)
mods_json <- fromJSON("data/swgoh-mods-sample.json")

mods_info <- data.frame(Mod.ID = mods_json$all_mods$mod_uid,
                        Initial.Toon = mods_json$all_mods$characterName,
                        Shape = mods_json$all_mods$slot,
                        Set = mods_json$all_mods$set,
                        Level = mods_json$all_mods$level,
                        Primary = mods_json$all_mods$primaryBonusType,
                        Primary.Value = mods_json$all_mods$primaryBonusValue,
                        Secondary1 = mods_json$all_mods$secondaryType_1,
                        Secondary1.Value = mods_json$all_mods$secondaryValue_1,
                        Secondary2 = mods_json$all_mods$secondaryType_2,
                        Secondary2.Value = mods_json$all_mods$secondaryValue_2,
                        Secondary3 = mods_json$all_mods$secondaryType_3,
                        Secondary3.Value = mods_json$all_mods$secondaryValue_3,
                        Secondary4 = mods_json$all_mods$secondaryType_4,
                        Secondary4.Value = mods_json$all_mods$secondaryValue_4,
                        Pips = mods_json$all_mods$pips,
                        stringsAsFactors = FALSE)

rm(mods_json)
head(mods_info)
##                   Mod.ID      Initial.Toon    Shape        Set Level
## 1 -2uujN4fSzq9Y5_Bo5B8ew       Poe Dameron   square      speed    15
## 2 -BCBuYQJSVGcfsicCBbx2A Biggs Darklighter triangle critdamage    15
## 3 -DSU8UhkTqyXQp1tuhSfWg     Darth Sidious triangle critdamage    12
## 4 -FGSr-lSREyxoRboW8LK4A        unassigned    arrow     health    15
## 5 -jpEqtA8Q9OQtm-z4gx0rQ        unassigned  diamond    potency    15
## 6 -Lf6errXS0OCiVMEKaUg5w        unassigned    cross critdamage    15
##           Primary Primary.Value      Secondary1 Secondary1.Value
## 1         Offense        +5.88%           Speed               +6
## 2         Defense       +11.75%        Health %             +1.1
## 3 Critical Chance        +9.75%       Defense %            +0.87
## 4         Offense        +1.88% Critical Chance            +1.6%
## 5         Defense       +11.75%         Offense              +69
## 6      Protection         +7.5%       Defense %            +0.78
##     Secondary2 Secondary2.Value Secondary3 Secondary3.Value Secondary4
## 1      Potency           +2.09%     Health             +347  Defense %
## 2        Speed               +4    Defense               +5    Offense
## 3        Speed               +5  Offense %            +0.54   Health %
## 4 Protection %             +0.6   Tenacity           +0.65%    Defense
## 5   Protection             +457    Potency           +1.25%      Speed
## 6        Speed               +1  Offense %            +0.23    Potency
##   Secondary4.Value Pips
## 1             +1.4    5
## 2              +38    5
## 3            +0.99    5
## 4               +2    1
## 5               +3    5
## 6           +0.59%    1

Since a typical user would have hundreds of mods, I filtered this list down to only contain the best; maximum pips, maximum level, and some speed enhancement (“speed is king”!). As I’ll discuss later, problem size becomes a real issue!

dim(mods_info)
## [1] 547  16
mods_info <- mods_info %>% filter(Pips == "5", 
                                  Level == "15", 
                                  (Primary == "Speed"
                                   |Secondary1=="Speed"
                                   |Secondary2=="Speed"
                                   |Secondary3=="Speed"
                                   |Secondary4=="Speed"))
dim(mods_info)
## [1] 108  16

Next, I get a list of unique MOD ID numbers and Toon names (looking back at this I should have used distinct(), filter(), and arrange() from dplyr):

mod_list <- mods_info$Mod.ID
toon_list <- unique(mods_info$Initial.Toon) %>% sort() 
toon_list <- toon_list[toon_list != "unassigned"]

Cleaning it up

Then some more cleaning up, replacing double quotes with single quotes, ensuring consistent naming, and converting to numerics:

toon_list <- stringr::str_replace_all(toon_list, "\"", "'")
mods_info$Initial.Toon <- stringr::str_replace_all(mods_info$Initial.Toon, "\"", "'")
mods_info$Shape <- tools::toTitleCase(mods_info$Shape)
mods_info$Set <- tools::toTitleCase(mods_info$Set)
mods_info$Set <-stringr::str_replace_all(mods_info$Set, "Critdamage", "Critical Damage")
mods_info$Set <-stringr::str_replace_all(mods_info$Set, "Critchance", "Critical Chance")
mods_info$Set <- mods_info$Set %>% paste("%")
mods_info$Primary <- mods_info$Primary %>% paste("%")
mods_info$Primary <- stringr::str_replace_all(mods_info$Primary, "Speed %", "Speed")

mods_info[,c("Secondary1",
             "Secondary2",
             "Secondary3",
             "Secondary4")] <- mods_info %>%
  select(Secondary1,Secondary2,
         Secondary3,Secondary4) %>%
  sapply(function(x) {gsub("Potency","Potency %",x)}) %>%
  sapply(function(x) {gsub("Tenacity","Tenacity %",x)}) %>%
  sapply(function(x) {gsub("Critical Chance","Critical Chance %",x)})

mods_info[,c("Primary.Value",
             "Secondary1.Value",
             "Secondary2.Value",
             "Secondary3.Value",
             "Secondary4.Value")] <- mods_info %>%
  select(Primary.Value,
         Secondary1.Value,
         Secondary2.Value,
         Secondary3.Value,
         Secondary4.Value) %>%
  sapply(function(x) {gsub("\\+","",x)}) %>%
  sapply(function(x) {gsub("%","",x)}) 

mods_info$Level <- as.numeric(mods_info$Level)
mods_info$Primary.Value <- as.numeric(mods_info$Primary.Value)
mods_info$Secondary1.Value <- as.numeric(mods_info$Secondary1.Value)
mods_info$Secondary2.Value <- as.numeric(mods_info$Secondary2.Value)
mods_info$Secondary3.Value <- as.numeric(mods_info$Secondary3.Value)
mods_info$Secondary4.Value <- as.numeric(mods_info$Secondary4.Value)
mods_info$Pips <- as.numeric(mods_info$Pips)

What I also wanted to do was make the numbers easy. Each toon could hold 6 mods (one of each shape), so I wanted to make sure I had enough mods to fit all the toons with none remaining, and enough toons to receive all mods with none remaining. This involves creating a number of “ghost mods” and “ghost toons”; the ghost mods “fill” the empty slots, and the ghost toons just hold the unassigned mods. First I calculate how many mods of each shape exist, and how many extra ghost toons are needed (in this case, none) and add them to the list:

(NoShapes <- map_int(slots, function(x) {mods_info %>% filter(Shape == x) %>% nrow()}))
## [1] 14 26 18 19 13 18
(NoGhostToons <- max(max(NoShapes) - length(toon_list),0))
## [1] 0
if (NoGhostToons > 0) {
  ghost_toon <- "Ghost toon"
  ghost_toons <- paste(ghost_toon, 1:NoGhostToons)
  toon_list <- c(toon_list, ghost_toons)
  rm(ghost_toon, ghost_toons)
}

Now the number of toons is fixed, I can calculate how many ghost mods I need of each shape, create their stats, and add them to the list. Note that the number of mods trebles for this sample input file:

(NoGhostShapes <- pmax(length(toon_list) - NoShapes,0))
## [1] 27 15 23 22 28 23
for (i in 1:length(slots)) {
  
  if (NoGhostShapes[i] > 0) {
    ghost_mod <- paste("Ghost", slots[i], "mod")
    ghost_mods <- paste(ghost_mod, 1:NoGhostShapes[i])
    mod_list <- c(mod_list, ghost_mods)
    extra_rows <- data.frame(ghost_mods,
                             "unassigned",
                             slots[i],
                             "None",
                             1,
                             NA,NA,NA,NA,NA,NA,NA,NA,NA,NA,
                             1,
                             stringsAsFactors = FALSE)
    names(extra_rows) <- names(mods_info)
    mods_info <- mods_info %>% bind_rows(extra_rows)
  }
}
rm(extra_rows, ghost_mod, ghost_mods)
dim(mods_info)
## [1] 246  16

The final piece of information needed is what weightings to give the various stats, and also what importance to place on various toons. The intention was to allow the user to focus on their most used toons, and to also allow the user to define which stats they would like the program to focus on enhancing for each toon. For now, I just assigned these weightings randomly with an integer:

set.seed(50)
toon_priority <- sample(0:20, 
                        size=length(toon_list),
                        replace = TRUE)
names(toon_priority) <- toon_list
toon_priority
##           Ahsoka Tano     Biggs Darklighter             Boba Fett 
##                    14                     9                     4 
##           Chief Nebit     CT-21-0408 'Echo'       CT-5555 'Fives' 
##                    16                    10                     0 
##         CT-7567 'Rex'            Darth Maul         Darth Nihilus 
##                    14                    13                     0 
##               Dathcha     Emperor Palpatine                  Finn 
##                     2                     8                     5 
## First Order TIE Pilot        General Kenobi     Geonosian Soldier 
##                    13                     1                     5 
##     Grand Master Yoda     Grand Moff Tarkin                 HK-47 
##                    14                    17                     7 
##  IG-86 Sentinel Droid                 IG-88            Ima-Gun Di 
##                     1                     3                    12 
##         Jawa Engineer        Jawa Scavenger    Jedi Knight Anakin 
##                     4                    14                    17 
##              Jyn Erso              Kylo Ren      Lando Calrissian 
##                     6                    13                    12 
##        Luke Skywalker       Luminara Unduli            Mace Windu 
##                     5                     6                     7 
##           Poe Dameron          Qui-Gon Jinn                 R2-D2 
##                     7                     8                     9 
##      Resistance Pilot    Resistance Trooper                   Rey 
##                    12                     4                    13 
##           Royal Guard           Sabine Wren                 Teebo 
##                     2                    14                     7 
##     TIE Fighter Pilot        Wedge Antilles 
##                     9                     7
toon_stat_priority <- matrix(sample(0:10,
                                    size = length(toon_list)*length(stat_names),
                                    replace = TRUE),
                             nrow = length(toon_list),
                             ncol = length(stat_names))
rownames(toon_stat_priority) <- toon_list
colnames(toon_stat_priority) <- stat_names
head(toon_stat_priority)
##                   Speed Speed % Potency % Tenacity % Offense Offense %
## Ahsoka Tano           9       3         6          7       2         1
## Biggs Darklighter     3       5         5          5       7         8
## Boba Fett             1       5         5          3       3         2
## Chief Nebit           0       0         2          4       9         1
## CT-21-0408 'Echo'     7       0         0          9       4         7
## CT-5555 'Fives'      10       7         0          4       5         9
##                   Protection Protection % Critical Chance %
## Ahsoka Tano                3            9                 7
## Biggs Darklighter          0           10                 3
## Boba Fett                  5            2                 4
## Chief Nebit                5            5                 8
## CT-21-0408 'Echo'         10            1                 4
## CT-5555 'Fives'           10            3                 8
##                   Critical Damage % Defense Defense % Health Health %
## Ahsoka Tano                       7       5         8      1        0
## Biggs Darklighter                 3       9         0      4        8
## Boba Fett                         9       7         6      5        0
## Chief Nebit                       6       7         9     10        8
## CT-21-0408 'Echo'                 2       9         8      1        7
## CT-5555 'Fives'                   4       0         3      5       10

Let the shenanigans begin!!

At this point it’s worth mentioning that my first effort was to try to get an optimisation algorithm working. In order to do that, I created a mod assignment array which represented all of the decision variables and basically consisted of 0s and 1s indicating whether a toon was equipped with that mod.

mod_assignment <- matrix(0, nrow=length(toon_list), ncol=length(mod_list))
rownames(mod_assignment) <- toon_list
colnames(mod_assignment) <- mod_list

I had input values and weightings, all I needed now was an objective function. In order to create one, I first needed to create a helper function that could update every toon’s stat enhancements when a new set of mods were applied. This function takes a particular toon and stat, and then uses the mod_assignment array to figure out which mods have been applied, adds up the stats from primaries and secondaries, and then adds on set bonuses:

new_stat <- function(toon, stat, mod_assignment) {
  
  
  toon_mods <- mod_list[mod_assignment[mod_assignment[toon,]>0]]
  
  new_stat <- mods_info %>% 
                filter(Mod.ID %in% toon_mods) %>% 
                filter(Primary == stat) %>% 
                select(Primary.Value) %>% 
                colSums() +
              mods_info %>% 
                filter(Mod.ID %in% toon_mods) %>% 
                filter(Secondary1 == stat) %>% 
                select(Secondary1.Value) %>% 
                colSums() +
              mods_info %>% 
                filter(Mod.ID %in% toon_mods) %>% 
                filter(Secondary2 == stat) %>% 
                select(Secondary2.Value) %>% 
                colSums() +
              mods_info %>% 
                filter(Mod.ID %in% toon_mods) %>% 
                filter(Secondary3 == stat) %>% 
                select(Secondary3.Value) %>% 
                colSums() +
              mods_info %>% 
                filter(Mod.ID %in% toon_mods) %>% 
                filter(Secondary4 == stat) %>% 
                select(Secondary4.Value) %>% 
                colSums()
  
  if (calc_bonuses <- TRUE) {
    
    num_bonuses <- ifelse(set_bonus_rules[stat,"Number"] == 0,
                          0,
                          mods_info %>% 
                            filter(Mod.ID %in% toon_mods, Set == stat) %>% 
                            nrow() %/% set_bonus_rules[stat,"Number"])
    
    num_max_bonuses <- ifelse(set_bonus_rules[stat,"Number"]==0,
                              0,
                              mods_info %>% 
                                filter(Mod.ID %in% toon_mods, Set == stat) %>% 
                                filter(Level == 15) %>% 
                                nrow() %/% set_bonus_rules[stat,"Number"])    
  } else {
    num_bonuses <- 0
    num_max_bonuses <- 0
  }
  
  new_stat <- new_stat + num_max_bonuses * set_bonus_rules[stat,"Max.Bonus"] +
    (num_bonuses - num_max_bonuses) * set_bonus_rules[stat,"Bonus"]
  return(as.numeric(new_stat))
}

Next, the objective function calculates the overall score, which we wish to maximise. However this is where things began to get a bit fluid, and I was changing my approach without any kind of version control like Git. I can’t remember how I came about using the setNames() function, but the first line of the function is effectively a vectorised nested for-loop, applying the new_stat() function to every combination of toon and stat. The result is a matrix of enhancements for every toon and stat. These are then normalised with the max_stats dataframe, and the overall score calculated using the priority weightings.

overall_score <- function(mod_assignment) {
  
  toon_stats <- setNames(object = data.frame(sapply(stat_names, 
                                  function(x) sapply(toon_list[1:(length(toon_list)-NoGhostToons)],
                                function(y) new_stat(y, x, mod_assignment))),
                                  row.names = toon_list[1:(length(toon_list)-NoGhostToons)]),
                         nm = stat_names) %>% as.matrix()
  
   toon_stats_norm <- sweep(toon_stats, 2, rowSums(max_stats), '/')
  

  overall_score <- toon_priority[1:(length(toon_list)-NoGhostToons)] *    rowSums(toon_stat_priority[1:(length(toon_list)-NoGhostToons)] * toon_stats_norm)
  
  return(sum(overall_score))
}

Since the mod assignment matrix should only take the values 0 and 1, and any one toon can only be assigned one mod of each shape, I struggled finding an R package that could deal with this kind of problem. I eventually found the rgenoud package, but it quickly dawned on me that the problem was simply FAR too big for memory and I needed to rescope. The really time consuming part was the setNames() function above, and I attempted several things to speed things up, including not calculating the stats for the ghost toons, but to no avail. I also found that the genetic algorithm in rgenoud needed to find a certain number of feasible solutions in order to create future generations of potential solutions, but constraining the algorithm was beyond my abilities, e.g. stopping it from assigning two square mods to a toon. There were simply too many infeasible solutions that could be created as the algorithm explored the decision space.

Attempt 2: Brute force…

I abandoned optimisation, and then attempted a partial brute-force approach, modifying the data structures to force only one of each shape mod being assigned. My plan was to treat the problem as 6 ‘independent’ problems, i.e. finding effective ways of assigning the square mods, effective ways of assigning the triangle mods, etc.

First I split up the mods list into a dataframe, with a column for each shape:

mod_list_shape <- map(slots, function(x) {mods_info$Mod.ID[mods_info$Shape == x]}) %>%
  as.data.frame()
names(mod_list_shape) <- slots
head(mod_list_shape)
##                   Square                Diamond                 Circle
## 1 -2uujN4fSzq9Y5_Bo5B8ew -jpEqtA8Q9OQtm-z4gx0rQ 5GJk_0AyS2a-1WAiJiRITg
## 2 a79U_UGvQmypgizQRx8Tzw 2n6fhh6gTbSLV4uxuvE0Lg 7MbqYLFpQVW7rxgPpoRFtw
## 3 BFtZ9Ij6Qpe_5Kf6WDm96w 33hCaqVtSJCyuZU3lR76Ww 8xlsn3eVTEuVu18JvRfBuA
## 4 bnwcjrsJQ0moWp-mvJlMrQ 3iIvjj-SSfy-E8sPm6eJpw 9bQzGF6cSYyeW0uZ8u7FCg
## 5 C5NMzQauRNWHpa2-ni31NQ 7dJZO6JYSfOQ89IMuljSLg a5AwI1sIROSDYCRYrFn9ag
## 6 cdUpBP2PQvyczmrEu9z-7Q 8x7VtTxRQrWhyWralRdlrg Ao8i6ES6QBqZiQKuIKwLtg
##                    Arrow               Triangle                  Cross
## 1 17zYpRcARLKlbRM5a-Tpkw -BCBuYQJSVGcfsicCBbx2A 26qCJ0PWR3CB8apb38l17g
## 2 1v6HSnVHSCOigsIn3x18Gg 48_oOrwMS1iWVAE-KOmVEQ 3v1_O4lbTAGINd3gsQ1O0w
## 3 AdHQrio1Ry-CjE4I3lab8A 6dA0oUFiSrOGomcaZxQUDA 5o7UuQ6vSmqKSP4qUlvBsQ
## 4 AlWHuiYGSReCuJERg7U1iw ACAy-17-TDqjtzqIWcns9g 6Xx4rsvoTxGBVeoCoECJSQ
## 5 aQsY--AqTd2kNTNCxPMSNQ ajhVqNkhR_SJgygX-R2SGg 74cDghpDSCmCyyLLj8Q_Aw
## 6 COV7jFELQFaN67a0eFhsdw G1RTvQBOTJeMadRUMh43FQ AH7-UZQ6Roa0u6__RxghNg

I then define how many permutations of mod assignments I’m going to generate, and how many of the best ones I’m going to use to find the best overall solution:

permutations <- length(toon_list)*length(mod_list)
top_permutations <- permutations %/% 10

I then create a number of data structures. First the mod_assignment array I used before is re-imagined, so that instead of each element being either 0 or 1, it now records the index of the appropriately shaped mod from the mod_list_shape dataframe. The slot_perms array is a temporary structure to hold all mod assignment permutations for a particular shape, the best of which get stored in the top_perms array. The top_perms array basically stores several versions of the mod_assignment array with the best scores.

mod_assignment <- matrix(0, nrow=length(toon_list), ncol=length(slots))
rownames(mod_assignment) <- toon_list
colnames(mod_assignment) <- slots

slot_perms <- matrix(0, nrow=permutations, ncol=length(toon_list))

top_perms <- array(0, c(top_permutations, length(toon_list), length(slots)))

The code below loops through each shape to find the best per-shape permutations. Since it’s operating on each shape independently, a variable calc_bonuses ensures that the overall_score function does not try to calculate set bonuses, since that requires looking across shapes in a mod set.

The first code chunk in the loop checks whether there are ghost mods AND ghost toons and then goes through each Ghost mod and permutation about to be generated and ensures it is assigned to a ghost toon (so that the problem space is reduced as much as possible). The second code chunk goes on to randomly assign the other mods.

The third code chunk populates the mod_assignment array and goes on to calculate the scores for each assignment permutation. The final chunk finds the top scoring assignment permutations. It’s worth noting that I wrote my own top_n() function, not realising one already existed in the dplyr package! I’ve left it out here and I’ve not tested whether the code still works using the dplyr function.

calc_bonuses <- FALSE
for (i in 1:length(slots)) {

    
  # put ghost mods with ghost toons  
  for (j in 1:NoGhostShapes[i]) {
    NoPreAssign <- min(NoGhostToons, NoGhostShapes[i])
    if (NoPreAssign > 0 ) {
      for (k in 1:permutations) {
        slot_perms[k, (length(toon_list) - NoPreAssign + 1):length(toon_list)] <- 
          (nrow(mod_list_shape) - NoPreAssign + 1):nrow(mod_list_shape)   
      }
    }
  }
  
  # fill out remaining slots of slot_perms with random mods
  for (k in 1:permutations) {
    slot_perms[k, 1:(length(toon_list) - NoPreAssign)] <- 
      sample(1:(nrow(mod_list_shape) - NoPreAssign),
             nrow(mod_list_shape) - NoPreAssign)
  }
  
  # get score of permutations
  # fill out i'th column of mod_assignment
  for (k in 1:permutations) {
    message(slots[i], ", ", k, " out of ", permutations, " permutations")
    mod_assignment[,i] <- slot_perms[k,]
    perm_scores[k] <- overall_score(mod_assignment)
  }
  
  # get the top X and store in a 3D array (perm, toon, slot)
  top_perms[1:top_permutations, 1:length(toon_list), i] <- 
    slot_perms[top_n(perm_scores, top_permutations),] 
 
   
}

Finally, the mod assignment array is reused to repeatedly generate assignments composed of random samples of the top performing mods for each slot. The perm_scores vector holds all the overall scores for each permutation.

calc_bonuses <- TRUE
perm_scores <- matrix(0, nrow=permutations, ncol=1)
for (k in 1:permutations) {
  mod_assignment <- matrix(c(top_perms[sample(1:top_permutations,1),,1],
                             top_perms[sample(1:top_permutations,1),,2],
                             top_perms[sample(1:top_permutations,1),,3],
                             top_perms[sample(1:top_permutations,1),,4],
                             top_perms[sample(1:top_permutations,1),,5],
                             top_perms[sample(1:top_permutations,1),,6]), 
                           nrow=length(toon_list), ncol=length(slots))
  rownames(mod_assignment) <- toon_list
  colnames(mod_assignment) <- slots
  perm_scores[k] <- overall_score(mod_assignment)
}

The eventual resignation…

The two approaches explained above are only a sample of approaches I tried, and I had tremendous difficulty getting any of them to produce something of value in reasonable runtimes. I was eventually left exploring approaches that would merely visualise potential assignments for users to choose from, cutting out some of the work involved. I reached a point where I concluded that, for now, the problem was too complex for me to produce something that had value (as it was probably designed to be).

One of the things I would do if I had to do it all again would be to make use of the map_* functions from the purrr package to get away from all that looping. I think when I wrote this code I was still trying to get my head around it conceptually. I dread to think how bad this code looks to the R gurus out there!

However, this little project really did cement my understanding of R data structures and gave me some invaluable R practice. I’d definitely be interested if anyone manages to crack this problem. From what I can tell, no one got as far as this from what I saw on the game forums, at least without doing optimisation on one toon at a time.

To leave a comment for the author, please follow the link and comment on their blog: R on R-house.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)