-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathReadMe.Rmd
More file actions
261 lines (204 loc) · 12.7 KB
/
ReadMe.Rmd
File metadata and controls
261 lines (204 loc) · 12.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
---
title: "Elden Ring Armorset Optimizer"
output: github_document
---
```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = TRUE, message = FALSE, warning = FALSE, comment = "")
```
## Introduction
<br>
I saw this report as an opportunity to prove the R skills that I picked up this semester and expand upon them. I chose an optimization problem because I thought that I would come to a satisfying conclusion after learning how to build a model in R. I picked this dataset and question because Elden Ring is one of my favorite games of all time. It is notoriously hard and I felt that if I could come to an answer that could make the game easier, it would be worth my time.
### Primary Questions
<br>
Given the Ultimate Elden Ring dataset, I wanted to figure out the best possible armor combinations against each of the different damage types. I also wanted to see how these armor combinations changed when you changed the player's weight and roll speed.This was an optimization problem where I was putting together the **best combination of armor pieces to minimize damage taken**.
## Data
The data that I am using for this project can be accessed from [Kaggle : Ultimate Elden Ring with Shadow of the Erdtree](https://www.kaggle.com/datasets/pedroaltobelli/ultimate-elden-ring-with-shadow-of-the-erdtree-dlc)
The first thing that I did was call in the packages I may need while wrangling this data
```{r}
library(dplyr)
library(tidyr)
```
Then I unzip the file in my Google Drive and bring in the two datasets I needed from the zip: the armor set and the weapons set. I will be performing my optimization based on the armor csv and the weapon csv is needed to add weapon weight into my calculations
```{r}
# project_files <- unzip('G:/My Drive/MSBA/MSBR 70280/archive.zip')
armors <- read.csv('C:/Users/liams/OneDrive/Documents/R Practice/er-optimizer/data/armors.csv')
weapons <- read.csv('C:/Users/liams/OneDrive/Documents/R Practice/er-optimizer/data/weapons.csv')
```
For the most part the data was clean, but there was some funky stuff going on in the damage negation column. Inside of each row, all of the 8 different damage types were held in this column. Before I could do anything, I needed to break this apart. I put the damage types into a blank data frame, then binded the damage data frame to the original armor one.
```{r}
armors$damage.negation <- gsub("\\[|\\]|\\{|\\}|'", "", armors$damage.negation)
init_split <- strsplit(armors$damage.negation, ",")
blank_matrix <- matrix(NA, ncol = 8, nrow = length(init_split))
for(i in 1:length(init_split)) {
blank_matrix[i, ] <- init_split[[i]]
}
blank_matrix <- as.data.frame(blank_matrix)
new_names <- c("phy", "vs_str", "vs_sla", "vs_pie",
"mag", "fir", "lit", "hol")
colnames(blank_matrix) <- new_names
blank_matrix[, new_names] <- lapply(new_names, function(x) {
gsub("^.*: ", "", blank_matrix[, x])
})
armors <- cbind(armors, blank_matrix)
```
Next I do some work in the armor data frame to make it easier to work with. I converted all of the damage resistances to numeric. I had to clean up the DLC column into being binary, as before it either had a 0, 1, or it read "Base Game". Finally, I selected the columns I needed for analysis.
```{r}
armors <- armors %>%
mutate(phy = as.numeric(phy),
vs_str = as.numeric(vs_str),
vs_sla = as.numeric(vs_sla),
vs_pie = as.numeric(vs_pie),
mag = as.numeric(mag),
fir = as.numeric(fir),
lit = as.numeric(lit),
hol = as.numeric(hol)) %>%
mutate(dlc = case_when(dlc == 'Base Game' ~ 0,
dlc == 0 ~ 0,
TRUE ~ 1)) %>%
select(1, 2, 5, 8, 12:20)
```
In order to make the optimization run smoother, I created dummy variables for each different armor piece. This allowed me to keep everything in one dataframe, as opposed to breaking it out by armor type.
```{r}
library(fastDummies)
armors <- dummy_cols(armors, select_columns = 'type')
names(armors)[names(armors) == 'type_chest armor'] <- 'type_chest'
names(armors)[names(armors) == 'type_leg armor'] <- 'type_leg'
```
The last "wrangling" I need to do is creating some objects that hold important numbers to my optimization. This means different potential player weights and our base weapon weight. I chose the base equip load based on a normal end game endurance level. I chose the weapon that I did because it was around the average weight of all weapons and it is something that you would probably be carrying around the end of the game. I also made the three different equip loads for each different roll type. Note: this weight does not include any talismans you may be wearing, just the weapon weight and armor weight.
```{r}
base_EL <- 84.1
weapon_weight <- weapons$weight[weapons$weapon_id == 154]
light_EL <- (0.299 * base_EL) - weapon_weight
med_EL <- (0.699 * base_EL) - weapon_weight
heavy_EL <- (0.99 * base_EL) - weapon_weight
```
## Methods
<br>
This armor optimization is a **bin packing** problem. I have what I want to maximize, damage negation, and I have a few constraints that my result set needs to follow. One constraint is that total weight needs to be less than the max weight that I set. Another is that there can only be at most one armor piece per piece type. This is because you cannot wear two chest pieces or two gauntlets at a time. I also create two optimization models, one of which has a constraint for only pulling items from the base game and one that includes items from the DLC.
<br>
First, I pull in the packages I need to optimize.
```{r}
library(ompr)
library(ompr.roi)
library(ROI.plugin.glpk)
library(magrittr)
```
Next, I create the different variables I need for my optimization function and for my mapply.
```{r}
armor_row <- nrow(armors)
obj_vars <- colnames(armors)[6:13]
el_vars <- c(light_EL, med_EL, heavy_EL)
obj_vars <- colnames(armors)[6:13]
option_combos <- expand.grid(obj_vars = obj_vars,
el_vars = el_vars)
option_combos$obj_vars <- as.character(option_combos$obj_vars)
```
Here is my optimization function with the base game constraint. This is what I will run through my mapply. This creates a function based on the restraints I talked about earlier. It then solves the function and brings it back to the complete data data frame.
```{r}
optim_function_base <- function(obj_vars, el_vars) {
obj_var_loop <- armors[, obj_vars]
full_model <- MIPModel() %>%
add_variable(x[i], i = 1:armor_row, type = 'binary') %>%
set_objective(sum_expr(x[i] * obj_var_loop[i], i = 1:armor_row), sense = 'max') %>%
add_constraint(sum_expr(armors$weight[i] * x[i], i = 1:armor_row) <= el_vars) %>%
add_constraint(sum_expr(armors$type_helm[i] * x[i], i = 1:armor_row) <= 1) %>%
add_constraint(sum_expr(armors$type_chest[i] * x[i], i = 1:armor_row) <= 1) %>%
add_constraint(sum_expr(armors$type_gauntlets[i] * x[i], i = 1:armor_row) <= 1) %>%
add_constraint(sum_expr(armors$type_leg[i] * x[i], i = 1:armor_row) <= 1) %>%
add_constraint(sum_expr(armors$dlc[i] * x[i], i = 1:armor_row) == 0)
model_solve <- solve_model(full_model, with_ROI('glpk'))
model_solution <- get_solution(model_solve, x[i])
complete_data <- cbind(armors, model_solution)
output <- complete_data[complete_data$value > 0, ]
output$obj_var <- obj_vars
output$el_vars <- el_vars
output
}
```
Next I put this into an mapply. The mapply runs through all the different possible damage resistances and equip loads and gives me a nice data frame with all the optimal combinations.
```{r}
all_combo_optim_base <- mapply(
optim_function_base,
option_combos$obj_vars[1:24],
option_combos$el_vars[1:24],
SIMPLIFY = FALSE
)
all_combo_optim_base <- do.call(rbind, all_combo_optim_base)
```
I then repeat this process without the base game constraint just to personally see what DLC items can be valuable.
```{r}
optim_function_full <- function(obj_vars, el_vars) {
obj_var_loop <- armors[, obj_vars]
full_model <- MIPModel() %>%
add_variable(x[i], i = 1:armor_row, type = 'binary') %>%
set_objective(sum_expr(x[i] * obj_var_loop[i], i = 1:armor_row), sense = 'max') %>%
add_constraint(sum_expr(armors$weight[i] * x[i], i = 1:armor_row) <= el_vars) %>%
add_constraint(sum_expr(armors$type_helm[i] * x[i], i = 1:armor_row) <= 1) %>%
add_constraint(sum_expr(armors$type_chest[i] * x[i], i = 1:armor_row) <= 1) %>%
add_constraint(sum_expr(armors$type_gauntlets[i] * x[i], i = 1:armor_row) <= 1) %>%
add_constraint(sum_expr(armors$type_leg[i] * x[i], i = 1:armor_row) <= 1)
model_solve <- solve_model(full_model, with_ROI('glpk'))
model_solution <- get_solution(model_solve, x[i])
complete_data <- cbind(armors, model_solution)
output <- complete_data[complete_data$value > 0, ]
output$obj_var <- obj_vars
output$el_vars <- el_vars
output
}
all_combo_optim_full <- mapply(
optim_function_full,
option_combos$obj_vars[1:24],
option_combos$el_vars[1:24],
SIMPLIFY = FALSE
)
all_combo_optim_full <- do.call(rbind, all_combo_optim_full)
```
## Results
<br>
I do not have any visualizations, but I do have two data frames each consisting of 24 different armor combinations. There is a combination for each damage type (8) and for each equip load (3). While this was not a statistics problem, I know that my model provided me with the best possible armor combination to negate a certain type of damage. The data frames are *all_combo_optim_base* and *all_combo_optim_full*. The second to last column in the data frames tells you which damage type it is optimizing based on, and the last column gives you the equip load constraint that it followed. If you were looking to stop physical damage at a medium equip load while playing the base game, you would wear the Bull-Goat Helm, Radahn's Lion Armor, Banished Knight Gauntlets, and Bull-Goat Greaves. If you include the DLC, you actually change your helm and leg armor, instead equipping the GreatJar and the Verdigris Greaves.
Follwoing are the main damage types and equip loads I focused on for my report. I will show how you can easily find them and what they look like in game.
<br>
This is the armor set for light rolling against slashing damage.
```{r}
lr_vs_slash <- all_combo_optim_base %>%
filter(obj_var == 'vs_sla',
el_vars == light_EL) %>%
select(name, type, weight, vs_sla)
lr_vs_slash
```

```{r , echo=FALSE, fig.cap="Optimal Armor for : Light Rolling Against Slashing Damage", out.width = '100%'}
# vs_photo <- 'C:/Users/liams/OneDrive/Documents/R Practice/er-optimizer/images/e1b99d2f-d2bb-472c-94dd-6df002fae08d.jpg'
# knitr::include_graphics(vs_photo)
```
<br>
Here is the armor set for medium rolling against magic damage.
```{r}
mr_vs_mag <- all_combo_optim_base %>%
filter(obj_var == 'mag',
el_vars == med_EL) %>%
select(name, type, weight, mag)
mr_vs_mag
```

```{r , echo=FALSE, fig.cap="Optimal Armor for : Medium Rolling Against Magic Damage", out.width = '100%'}
# mag_photo <- 'C:/Users/liams/Downloads/5dad67e0-41be-4f68-bf22-3fcc1748d41b.PNG'
# knitr::include_graphics(mag_photo)
```
<br>
And here is the armor set for heavy rolling against holy damage, allowing items from the DLC.
```{r}
hr_vs_hol <- all_combo_optim_full %>%
filter(obj_var == 'hol',
el_vars == heavy_EL) %>%
select(name, type, weight, hol)
hr_vs_hol
```

```{r , echo=FALSE, fig.cap="Optimal Armor for : Heavy Rolling Against Holy Damage", out.width = '100%'}
# hol_photo <- 'C:/Users/liams/Downloads/059182d5-b899-4bda-8e33-96e1dd28da06.PNG'
# knitr::include_graphics(hol_photo)
```
## Discussion
<br>
When playing a video game that takes some over 100 hours to complete in full, there are plenty of decisions to be made. If you are making these decisions without any guidance, the choices you make may make your life a lot harder. I think that my findings are interesting and significant because they can be applied to pretty much any situation the game puts you in to. As long as you are able to find the armor it recommends, you can set yourself up to dominate any encounter if you know what type of damage type you will be facing.