The goal of this analysis is to provide basis for a function to generate a realistic fortress room and enemies given the following parameters:
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))
ggplot(groupedByRoomLevel, aes(x=roomLevel, y=nRuns)) + geom_bar(stat="identity") + ylab("Number of runs") + xlab("Room level") + theme_bw()
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")
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))
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))
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))")
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)")
I use a static probability value (10%) for each enemy to be an elite. This seems to be independent of difficulty / room level / etc.
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")