this is how I roll

The game of Warmachine has a rather nice mechanic for combat. The sum of two (or more) dice are added to the attackers’ melee attack stat (MAT), and these must equal or exceed the defender’s defence stat (DEF). This is great as it means that high and low skill warriors can be represented and quickly calculated without endless references to charts. It also means that average dice are more likely than extreme highs and lows. An exception is that snake-eyes always miss. Damage is applied in a same way. Damage caused by the attack is the sum of two (or more) dice and the power (POW), or power plus strength, less the armour value (ARM) of the defender.

ImageWe can work out likely outcomes of combat since we know all possible outcomes of rolling two dice. I created a function to return combinations of dice rolls using the permutations function from the package gtools. This is a really cool feature of R. Modules of code can just be dumped into your session. I used it to calculate all outcomes on n dice, work out all of the totals and count up how many there are of each. This is not the most elegant solution, but it works.

> require(gtools)
Loading required package: gtools
> # get distribution of dice rolls
> #
> # calculate the  value of each possible option when rolling dice
> # n, the number of dice
> # D, the number of sides
> 
> diceDistr <- function(n, D = 6) {
+    
+     if (length(n) != 1) {
+    
+         warning("length n should equal 1, using n[1]")
+        
+         n <- n[1]
+     }
+    
+     nvals <- D^n
+    
+     vals <- seq.int(from = n, to = nvals)
+    
+     combs <- permutations(D, n, v = seq_len(D),
+         set = FALSE, repeats.allowed = TRUE)
+        
+     totals <- apply(combs, 1, sum)
+    
+     tabTotals <- c(table(totals))
+    
+     tabFracs <- tabTotals / sum(tabTotals)
+    
+     return(tabFracs)
+ }
> # TEST 1: 2D6
> test1 <- diceDistr(n = 2, D = 6)
> all(test1 == c(1:6, 5:1) / 36)
[1] TRUE

Once we know the probability of each number, we can find the likelihood of a successful attack. The function cumsum is useful; it finds the cumulative sum of a numeric vector. We want to know the probability of rolling 7 or greater, so rev lets us reverse the order of the vector.

> round(rev(cumsum(rev(diceDistr(n = 2, D = 6))) * 100))
  2   3   4   5   6   7   8   9  10  11  12
100  97  92  83  72  58  42  28  17   8   3

Image

A Steelhead halberdier has MAT 5, Ragman has DEF 14. The Steelhead is hitting on nines, so he has a 28% chance of catching the slippery Thamarite.

Of course snake-eyes is a miss, so I need to adjust how we calculate these probabilities. This small function is one possible implementation.

> # probability that mat + dice is >= def
>
> probSuccess <- function(mat, def, n) {
+
+     dProb <- diceDistr(n)
+    
+     success <- mat + as.numeric(names(dProb)) >= def
+    
+     # all 1 always fails
+     success[1] <- FALSE
+    
+     # all 6s always succeeds
+     success[length(success)] <- TRUE
+    
+     prob <- sum(dProb[success])
+     return(prob)
+ }
> 
> # TEST 1 mat 5 vs def 14 == 28%
> test1 <- probSuccess(mat = 5, def = 14, n = 2)
> round(test1 * 100) == 28
[1] TRUE
>
> # TEST 2 mat 1 vs def 4 == 99.5%
> test2 <- probSuccess(mat = 1, def = 4, n = 3)
> test2 == 1 - 1/6^3
[1] TRUE
>
> # TEST 3 mat 1 vs def 19, 3D6 == 0.5%
> test3 <- probSuccess(mat = 1, def = 19, n = 3)
> test3 == 1/6^3
[1] TRUE

Using this function we can create a table of probabilities. I created a 3D numeric object called an array. This gave me every combination of MAT and DEF. I used these to pass the pairs of MAT and DEF to probSuccess to create the table. The function apply performs a loop across an object, in this case, summarizing the array.

> MAT <- seq_len(15)
> DEF <- seq_len(20)
> # rows of mat
> matMat <- matrix(rep(MAT, times = length(DEF)), nrow = length(MAT))
> # columns of def
> defMat <- matrix(rep(DEF, each = length(MAT)), nrow = length(MAT))
>
> # tabulation array
> cArray <- array(c(matMat, defMat),
+     dim = c(length(MAT), length(DEF), 2), 
+     dimnames = list(MAT, DEF, c("MAT", "DEF")))
> probTable2 <- apply(cArray, 1:2, function(x) { 
+     probSuccess(mat = x[1], def = x[2], n = 2) })
> dimnames(probTable2) <- list(MAT, DEF)
> round(probTable2[7:14, 5:18] * 100)
    5  6  7  8  9 10 11 12 13 14 15 16 17 18
7  97 97 97 97 97 97 92 83 72 58 42 28 17  8
8  97 97 97 97 97 97 97 92 83 72 58 42 28 17
9  97 97 97 97 97 97 97 97 92 83 72 58 42 28
10 97 97 97 97 97 97 97 97 97 92 83 72 58 42
11 97 97 97 97 97 97 97 97 97 97 92 83 72 58
12 97 97 97 97 97 97 97 97 97 97 97 92 83 72
13 97 97 97 97 97 97 97 97 97 97 97 97 92 83
14 97 97 97 97 97 97 97 97 97 97 97 97 97 92
> require(colorspace)
Loading required package: colorspace
> image(x = MAT, y = DEF, z = probTable2, 
+     col = diverge_hcl(60, c = 100, l = (range(probTable2) + 1) * 45, power = 1))
> 
> text(seq(2, 12, by = 2), 
+     seq(18, 8, by = -2), 
+         labels = round(probTable2[seq(2, 12, by = 2), 
+             seq(18, 8, by = -2)][as.logical(diag(6))] * 100))

mat_def_table2

> # get prob of success for each combination, 3 dice
> probTable3 <- apply(cArray, 1:2, function(x) {
+     probSuccess(mat = x[1], def = x[2], n = 3) })
> dimnames(probTable3) <- list(MAT, DEF)
> round(probTable3[7:14, 5:18] * 100, 1)
      5    6    7    8    9   10   11   12   13   14   15   16   17   18
7  99.5 99.5 99.5 99.5 99.5 99.5 99.5 98.1 95.4 90.7 83.8 74.1 62.5 50.0
8  99.5 99.5 99.5 99.5 99.5 99.5 99.5 99.5 98.1 95.4 90.7 83.8 74.1 62.5
9  99.5 99.5 99.5 99.5 99.5 99.5 99.5 99.5 99.5 98.1 95.4 90.7 83.8 74.1
10 99.5 99.5 99.5 99.5 99.5 99.5 99.5 99.5 99.5 99.5 98.1 95.4 90.7 83.8
11 99.5 99.5 99.5 99.5 99.5 99.5 99.5 99.5 99.5 99.5 99.5 98.1 95.4 90.7
12 99.5 99.5 99.5 99.5 99.5 99.5 99.5 99.5 99.5 99.5 99.5 99.5 98.1 95.4
13 99.5 99.5 99.5 99.5 99.5 99.5 99.5 99.5 99.5 99.5 99.5 99.5 99.5 98.1
14 99.5 99.5 99.5 99.5 99.5 99.5 99.5 99.5 99.5 99.5 99.5 99.5 99.5 99.5
>
> image(x = MAT, y = DEF, z = probTable3, 
+     col = diverge_hcl(60, c = 100, 
+         l = (range(probTable3) + 1) * 45, power = 1))
> 
> text(seq(2, 12, by = 2), 
+     seq(18, 8, by = -2), 
+         labels = round(probTable3[seq(2, 12, by = 2), 
+             seq(18, 8, by = -2)][as.logical(diag(6))] * 100))

mat_def_table3So no problems there. Attack roll probabilities are identical for a given difference between the MAT and DEF. These tables will be useful for working out how combat actions are likely to progress.

Advertisements

2 thoughts on “this is how I roll

  1. Pingback: smashing times | analytical gaming

  2. Pingback: do you wanna be in my gang? | analytical gaming

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s