Earthdawn is a venerable and grossly underrated RPG first published by FASA corporation somewhere in the nineties. It’s my favorite setting of all times, and I’m lucky enough to still be playing it today with my friends.
Earthdawn’s mechanics however are… a bit on the dated side and very peculiar. Rather than just rolling a die and adding some bonus or penalty to it, Earthdawn has something called “steps”.
Each step value has an associated die, or combination of dice to roll to achieve your result. This result is then compared to a difficulty number (like an enemy physical defense rating), if you rolled equal to, or higher than, the difficulty you have succeeded.
The system further boasts levels of success, but the biggest hardship when running any kind of analysis stems from the fact that it uses open ended die-rolling.
This means that if you roll the maximum face on any die, you get to reroll it and add the value to your total. This keeps happening until you roll lower than the maximum number on that particular die.
I was of a mind to model this, but after poking around a bit the math on “exploding dice” proved far (faaaaar) beyond me.
Fortunately, the law of large numbers is a thing, so I opted to simulate a suitably large number of rolls instead.
In order to accomplish this I created a few functions in R:
First I needed to make a “die roller” that could roll an n-sided “exploding” die. I came up with this (which may not be optimal in any way or form)
roll <- function(n){
result <- 0
repeat{
roll <- sample(1:n, 1)
result <- result + roll
if(roll<n){
break
}
}
return(result)
}
Next we needed a function capable of rolling more than one exploding die, and add up the results. The easiest way I could thing of was make a little function that iterates over a vector (q) of “die faces” like so:
rollstep <- function(q){
output <- 0
for(i in 1:length(q)) {
die <- roll(q[i])
output <- (output + die)
}
return(output)
}
Lastly, I made this ugly bit of code that creates a list of vectors, which in turn contain the dice for each step. This function can be passed the appropriate “step number” (from 4 to 40, as per the rulebook) and it will look up and roll the appropriate dice for you:
step <- function(n=4){
dice <- vector(mode="list", length=40)
dice[[1]] <- 6
dice[[2]] <- 8
dice[[3]] <- 10
dice[[4]] <- 12
dice[[5]] <- c(6,6)
dice[[6]] <- c(6,8)
dice[[7]] <- c(8,8)
dice[[8]] <- c(10,8)
dice[[9]] <- c(10,10)
dice[[10]] <- c(12,10)
dice[[11]] <- c(12,12)
dice[[12]] <- c(12,6,6)
dice[[13]] <- c(12,8,6)
dice[[14]] <- c(12,8,8)
dice[[15]] <- c(12,10,8)
dice[[16]] <- c(12,10,10)
dice[[17]] <- c(12,12,10)
dice[[18]] <- c(12,12,12)
dice[[19]] <- c(12,12,6,6)
dice[[20]] <- c(12,12,8,6)
dice[[21]] <- c(12,12,8,8)
dice[[22]] <- c(12,12,10,8)
dice[[23]] <- c(12,12,10,10)
dice[[24]] <- c(12,12,12,10)
dice[[25]] <- c(12,12,12,12)
dice[[26]] <- c(12,12,12,6,6)
dice[[27]] <- c(12,12,12,8,6)
dice[[28]] <- c(12,12,12,8,8)
dice[[29]] <- c(12,12,12,10,8)
dice[[30]] <- c(12,12,12,10,10)
dice[[31]] <- c(12,12,12,12,10)
dice[[32]] <- c(12,12,12,12,12)
dice[[33]] <- c(12,12,12,12,6,6)
dice[[34]] <- c(12,12,12,12,8,6)
dice[[35]] <- c(12,12,12,12,8,8)
dice[[36]] <- c(12,12,12,12,10,8)
dice[[37]] <- c(12,12,12,12,10,10)
q <- dice[[n-3]]
result <- rollstep(q)
return(result)
}
I realize I could have probably figured out a way to generate the (regular) dice sequence automatically, but frankly it was just as easy to copy it from the rulebook.
Next, I ran 20,000 simulations for each of the 37 steps. This took a while.
monte <- data.frame(stringsAsFactors = F)
names(monte) <- seq(4,40,1)
for(i in 1:20000){
storage <- vector(mode="numeric")
print(paste("Simulation", i, "of 20000"))
for(q in 4:40){
storage <- c(storage, step(q))
}
monte <- rbind(monte,storage)
}
Being a smart cookie, I immediately saved the result to a nice .csv file. If, for whatever reason, you’re following along at home, you may find it here: https://github.com/mvellinger/ED/blob/master/values.csv
Having my massive .csv of data handy, we can begin!
values <- read.csv("values.csv")
head(values, n=3)
## X4 X5 X6 X7 X8 X9 X10 X11 X12 X13 X14 X15 X16 X17 X18 X19 X20 X21 X22
## 1 3 10 5 11 4 9 12 7 11 14 7 24 13 5 50 17 5 13 33
## 2 7 15 5 1 6 6 9 6 10 7 11 14 8 10 25 17 37 18 21
## 3 5 1 6 4 9 11 9 9 11 23 9 17 10 11 23 18 17 12 13
## X23 X24 X25 X26 X27 X28 X29 X30 X31 X32 X33 X34 X35 X36 X37 X38 X39 X40
## 1 31 10 43 16 35 11 38 29 40 26 43 56 46 53 40 40 28 38
## 2 16 23 44 28 29 17 27 22 23 37 13 47 25 43 47 37 60 74
## 3 18 27 30 29 45 30 26 26 30 23 29 33 39 43 38 23 51 38
What we’re after here, is calculating what the chance of success for each “step” is, versus a wide range of difficulty numbers (let’s say from difficulty 4 to 40, same as our steps). Basically, a table.
The best way to do this is most likely a loop that creates a vector of percentages for each “step” and simply using ‘rbind()’ to glue them together.
Let’s do just that!
library(scales)
steplist <- seq(4,40,1)
steptable <- data.frame(stringsAsFactors = F, step=steplist)
for(j in 1:37){ #loop over our steps
percentages <- vector(mode="numeric")
for(i in 4:40){ #loop through our difficulty numbers
#glue our outcomes into the container vector
percentages <- c(percentages, round(length(which(values[ ,j]> (i-1) ))/20000, 3))
}
#append our vector as a new column
steptable <- cbind(steptable, percentages, check.rows=F)
}
#fix our column names
names(steptable) <- c("step", seq(4,40,1))
now we make a neat little table using the DataTables package:
library(DT)
datatable(steptable,
caption="Step vs. Difficulty chance of success.",
class = 'cell-border stripe',
rownames = FALSE,
options = list(
autoWidth = TRUE,
columnDefs = list(list(width = '40px', targets = 0)),
pageLength = 40
)
) %>% formatPercentage(2:38)