Goal

The goal of this analysis is to provide basis for a function to generate a realistic fortress room and enemies given the following parameters:

Data load and preparation

source(paste0("./fortressRunDataLoad.R"))
## Warning: package 'zoo' was built under R version 3.5.3
## 
## Attaching package: 'zoo'
## The following objects are masked from 'package:base':
## 
##     as.Date, as.Date.numeric
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:data.table':
## 
##     between, first, last
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
## [1] "Loaded 101 overall unique runs"
## [1] "Loaded 92 unique runs with 1 player"
dataTrain <- groupedByRoomLevel[roomLevel>0]
dataToPredict <- data.table(roomLevel=1:20, difficulty=playerMultiplier * getSoloDifficultyForRoomLevel(1:20), difficultyBeforePlayerMultiplier=getSoloDifficultyForRoomLevel(1:20))

Dataset size:

ggplot(groupedByRoomLevel, aes(x=roomLevel, y=nRuns)) + geom_bar(stat="identity") + ylab("Number of runs") + xlab("Room level") + theme_bw()

Difficulty

When removing the player count multiplier (see link), difficulty clearly scales quadratically with room number (for a single player):

ggplot(groupedByRun[runestoneLevels==1], aes(x=roomLevel, y=difficultyBeforePlayerMultiplier)) + geom_point() + geom_line() +
  xlab("Room level") + ylab("Difficulty before player count multiplier") + theme_bw() + ggtitle("Difficulty before player count multiplier is clearly quadratic")

Number of enemies (in relation to difficulty)

The following charts all use difficulty for the X axis. Hopefully this number alone can be used to describe the following characteristic of a fortress room:

I explicitly use difficulty instead of a combination of runestone levels, player count and room level.

For this graph, I count the number of elites as 2 enemies.

I use the following equation to predict number of enemies for a room: $ numberEnemies = + sqrt(difficulty) $

modelNEnemies <- lm(meanNEnemies ~ I(sqrt(difficulty)), dataTrain)
summary(modelNEnemies)
## 
## Call:
## lm(formula = meanNEnemies ~ I(sqrt(difficulty)), data = dataTrain)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.63242 -0.19977 -0.06599  0.22132  0.54675 
## 
## Coefficients:
##                     Estimate Std. Error t value Pr(>|t|)    
## (Intercept)         1.721327   0.126843   13.57 4.93e-13 ***
## I(sqrt(difficulty)) 0.066137   0.001884   35.10  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.3531 on 25 degrees of freedom
## Multiple R-squared:  0.9801, Adjusted R-squared:  0.9793 
## F-statistic:  1232 on 1 and 25 DF,  p-value: < 2.2e-16
(title <- paste0("let nEnemies = ", modelNEnemies$coefficients[[1]], " + ", modelNEnemies$coefficients[[2]],  " * Math.sqrt(difficulty);"))
## [1] "let nEnemies = 1.72132702158096 + 0.0661370810873348 * Math.sqrt(difficulty);"
ggplot(groupedByRoomLevel, aes(x=difficulty, y=meanNEnemies))  + 
  geom_point(aes(y=minNEnemies), color="red") + 
  geom_point(aes(y=maxNEnemies), color="red") + geom_point() + theme_bw() + xlab("difficulty") + ylab("Average number of enemies") + 
  geom_line(data=data.table(x=dataTrain$difficulty, y=predict(modelNEnemies, dataTrain)), aes(x=x, y=y))

Average enemy level (in relation to difficulty)

Here, the picture is clearer. The average enemy level seems to be a root function in relation to difficulty (which would make it linear in relation to room level).

I use the following equation to predict the average enemy level in a room: $ averageEnemyLevel = + averageRunestoneLevel + roomLevel $

modelAverageEnemyLevel <- lm(averageEnemyLevel ~ runestoneLevels + roomLevel, dataTrain) #

ggplot(groupedByRoomLevel, aes(x=roomLevel, y=averageEnemyLevel)) +
  geom_point() + xlab("Difficulty") + ylab("Average enemy level") + theme_bw() + xlab("Room level") + ylab("Mean number of enemies") +
  geom_line(data=data.table(x=dataTrain$roomLevel, y=predict(modelAverageEnemyLevel, dataTrain)), aes(x=x, y=y))

Computing a difficulty “budget” per enemy

When generating a room one approach could be to compute a difficulty “budget” per enemy, which would translate to enemy difficulty and enemy level.

This post suggests the following might hold:

$ overallDifficulty 2 * enemyDifficulty * enemyLevel (1+isElite)$

This relation seems to be a good approach for room levels > 7 but inaccurate for easier rooms (room levels <= 7).

The following chart shows how the factor 2 may be computed. I thus use the following equation to compute a “normalization” factor: $ difficultyBudgetPerEnemyMultiplier = + exp(-roomLevel) $ $ normalizedDifficultyBudgetPerEnemy = overallDifficulty / (difficultyBudgetPerEnemyMultiplier * nEnemies) $

ggplot(groupedByRun, aes(x=as.factor(roomLevel), y=difficulty/sumProposedMultiplication)) + geom_boxplot() + ylim(c(0, NA)) +
  theme_bw() + xlab("Room level") + ylab("Difficulty / sum(enemyLevel*enemyDifficulty*(1+isElite))") 

Average enemy difficulty (stars)

More data is needed here. The average difficulty seems strongly influenced by the number of enemies in the room.

I use the following equation to compute an average enemy difficulty: $ averageEnemyDifficulty = normalizedDifficultyBudgetPerEnemy / averageEnemyLevel $

ggplot(groupedByRoomLevel, aes(x=difficulty, y=averageEnemyDifficulty)) + geom_point(aes(y=minAverageEnemyDifficulty), color="red") + geom_point(aes(y=maxAverageEnemyDifficulty), color="red") + 
  geom_point() + ylim(c(1,5)) + theme_bw()+ xlab("Difficulty") + ylab("Average enemy difficulty (stars)")

Elite probability

I use a static probability value (10%) for each enemy to be an elite. This seems to be independent of difficulty / room level / etc.

Average proficiency

The current theory is that when doing a solo fortress run you will be proficient against a certain percentage of enemies. I have seen values between 50% and 60%.

This graph disputes that theory and shows that this percentage may depend on difficulty/room level and is not in fact constant. However, more data is needed to model this relation.

ggplot(groupedByRoomLevel, aes(x=as.factor(roomLevel), y=averageProficiency)) + geom_boxplot() + theme_bw() + xlab("Room level") + ylab("Average percentage the wizard was proficient against")